mirror of
https://github.com/Theodor-Springmann-Stiftung/musenalm.git
synced 2026-02-04 10:35:30 +00:00
FIX: edit pages for contents could not be reached bc of trtailing slash error
This commit is contained in:
@@ -63,7 +63,7 @@
|
||||
{{- if $model.result.HasContents -}}
|
||||
<div
|
||||
id="almanachcontents"
|
||||
hx-get="/almanach/{{ $model.result.Entry.MusenalmID }}/contents/{{- if $model.request.query -}}?{{- $model.request.query -}}{{- end -}}"
|
||||
hx-get="/almanach/{{ $model.result.Entry.MusenalmID }}/contents-view/{{- if $model.request.query -}}?{{- $model.request.query -}}{{- end -}}"
|
||||
hx-trigger="load"
|
||||
hx-target="this"
|
||||
hx-swap="outerHTML"
|
||||
|
||||
80
views/routes/almanach/components/contents.gohtml
Normal file
80
views/routes/almanach/components/contents.gohtml
Normal file
@@ -0,0 +1,80 @@
|
||||
{{ $model := . }}
|
||||
|
||||
|
||||
<div class="container-oversize mt-0 pt-0" id="almanachcontents">
|
||||
<div class="flex relative justify-center">
|
||||
<div class="-translate-y-[50%] flex flex-col items-center">
|
||||
<h2 class="relative bg-stone-50 px-5 font-bold text-3xl w-max mb-1">Inhalt</h2>
|
||||
<div class="flex flex-row justify-center">
|
||||
<div class="bg-stone-200 text-sm px-3 py-0.5 rounded mt-1">
|
||||
{{- if $model.filters.Type -}}
|
||||
{{- $i := len $model.result.Contents -}}
|
||||
{{- if eq $i 1 -}}
|
||||
<b>{{- $i }}</b> Beitrag der Kategorie <b>{{ $model.filters.Type }}</b> ·
|
||||
{{- else -}}
|
||||
<b>{{- $i }}</b> Beiträge der Kategorie <b>{{ $model.filters.Type }}</b> ·
|
||||
{{- end -}}
|
||||
{{- else if $model.filters.OnlyScans -}}
|
||||
<i class="ri-image-line"></i>
|
||||
{{- $i := len $model.result.Contents -}}
|
||||
{{- if eq $i 1 -}}
|
||||
<b>{{- $i }} Digitalisat · </b>
|
||||
{{- else -}}
|
||||
<b>{{- $i }} Digitalisate ·</b>
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
{{- if eq (len $model.result.Contents) 1 -}}
|
||||
<b>{{- len $model.result.Contents }}</b> erfasster Beitrag ·
|
||||
{{- else -}}
|
||||
<b>{{- len $model.result.Contents }}</b> erfasste Beiträge ·
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
<i class="ri-sort-number-asc"></i> Anzeige nach Reihenfolge
|
||||
</div>
|
||||
{{- if or $model.filters.Type $model.filters.OnlyScans -}}
|
||||
<div class="flex flex-row items-center justify-center ml-2.5">
|
||||
<div class="block bg-stone-200 text-sm px-3 py-0.5 rounded mt-1">
|
||||
<i class="ri-arrow-left-long-line"></i>
|
||||
<a href="/almanach/{{ $model.result.Entry.MusenalmID }}/contents-view/" hx-target="#almanachcontents" hx-select="#almanachcontents" hx-swap="outerHTML show:none" hx-indicator="body"> Alle Beiträge anzeigen </a>
|
||||
</div>
|
||||
</div>
|
||||
{{- end -}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex flex-row justify-end">
|
||||
{{- if gt (len $model.result.Types) 1 -}}
|
||||
<div>
|
||||
<form method="GET" hx-boost="false" x-target="almanachcontents" aria-label="Filter für Beitragstypen">
|
||||
<label for="typefilter" class="align-baseline h-min self-end pb-1 mr-1.5 text-sm font-sans text-stone-700"> Kategorie </label>
|
||||
<select class="h-min pb-1 border-b-4 border-zinc-300 px-1.5 mr-8" name="typefilter" id="typefilter" autocomplete="off" @change.debounce="$el.form.requestSubmit()">
|
||||
<option value="">Alle</option>
|
||||
{{- range $i, $t := $model.result.Types -}}
|
||||
<option value="{{- $t -}}" {{- if eq $model.filters.Type $t -}}selected{{- end -}}>
|
||||
{{- $t -}}
|
||||
</option>
|
||||
{{- end -}}
|
||||
</select>
|
||||
<button x-show="false">Filtern</button>
|
||||
</form>
|
||||
</div>
|
||||
{{- end -}}
|
||||
{{- if $model.result.HasScans -}}
|
||||
<div>
|
||||
<form method="GET" hx-boost="false" x-target="almanachcontents" aria-label="Filter für digialisierte Beiträge">
|
||||
<label for="onlyscans" class="align-baseline h-min self-end pb-1 mr-1.5 text-sm font-sans text-stone-700"> Nur Digitalisate anzeigen </label>
|
||||
<input class="" type="checkbox" id="onlyscans" name="onlyscans" autocomplete="off" @change.debounce="$el.form.requestSubmit()" {{ if $model.filters.OnlyScans -}}checked{{- end -}} />
|
||||
<button x-show="false">Filtern</button>
|
||||
</form>
|
||||
</div>
|
||||
{{- end -}}
|
||||
</div>
|
||||
<div class="mt-8">
|
||||
{{- range $i, $c := $model.result.Contents -}}
|
||||
{{- $rels := index $model.result.ContentsAgents $c.Id -}}
|
||||
{{- template "_content" Arr $c $model.result.Entry $rels $model.result.Agents false false false $model.request -}}
|
||||
{{- end -}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,7 +57,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<i class="ri-file-edit-line"></i>
|
||||
<a href="/almanach/{{ $model.result.Entry.MusenalmID }}/contents/edit/">Beiträge bearbeiten</a>
|
||||
<a href="/almanach/{{ $model.result.Entry.MusenalmID }}/contents/edit">Beiträge bearbeiten</a>
|
||||
</div>
|
||||
{{- end -}}
|
||||
</div>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{{ template "_contents_detail" . }}
|
||||
@@ -1,948 +0,0 @@
|
||||
{{ $model := . }}
|
||||
|
||||
<edit-page>
|
||||
<div class="flex container-normal bg-slate-100 mx-auto px-8">
|
||||
<div class="flex flex-row w-full justify-between">
|
||||
<div class="flex flex-col justify-end-safe flex-2/5">
|
||||
<div class="mb-1">
|
||||
<div class="header-tabs">
|
||||
{{- if $model.result -}}
|
||||
<a href="/almanach/{{- $model.result.Entry.MusenalmID -}}/edit/" class="header-tab">
|
||||
<i class="ri-book-line"></i> Almanach
|
||||
</a>
|
||||
{{- else -}}
|
||||
<span class="header-tab header-tab-disabled">
|
||||
<i class="ri-book-line"></i> Almanach
|
||||
</span>
|
||||
{{- end -}}
|
||||
<span class="header-tab header-tab-active">
|
||||
<i class="ri-file-list-3-line"></i> Beiträge
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="text-2xl w-full font-bold text-slate-900 mb-1">
|
||||
{{- if $model.result -}}
|
||||
{{- $model.result.Entry.PreferredTitle -}}
|
||||
{{- else -}}
|
||||
Beiträge bearbeiten
|
||||
{{- end -}}
|
||||
</h1>
|
||||
{{- if $model.result -}}
|
||||
<div class="flex flex-row gap-x-3">
|
||||
<div>
|
||||
<a
|
||||
href="/almanach/{{ $model.result.Entry.MusenalmID }}"
|
||||
class="text-gray-700 hover:text-slate-950 block no-underline">
|
||||
<i class="ri-eye-line"></i> Anschauen
|
||||
</a>
|
||||
</div>
|
||||
·
|
||||
<div class="flex flex-row">
|
||||
{{- if $model.result.PrevByID -}}
|
||||
<div>
|
||||
<a href="/almanach/{{ $model.result.PrevByID.MusenalmID }}/contents/edit/" class="text-gray-700 hover:text-slate-950 no-underline block">
|
||||
<i class="ri-arrow-left-s-line"></i>
|
||||
</a>
|
||||
</div>
|
||||
{{- end -}}
|
||||
<div class="px-1.5 py-0.5 rounded-xs bg-gray-200 w-fit font-bold">
|
||||
{{ $model.result.Entry.MusenalmID }}
|
||||
</div>
|
||||
{{- if $model.result.NextByID -}}
|
||||
<div>
|
||||
<a href="/almanach/{{ $model.result.NextByID.MusenalmID }}/contents/edit/" class="text-gray-700 hover:text-slate-950 no-underline block">
|
||||
<i class="ri-arrow-right-s-line"></i>
|
||||
</a>
|
||||
</div>
|
||||
{{- end -}}
|
||||
</div>
|
||||
</div>
|
||||
{{- end -}}
|
||||
</div>
|
||||
{{- if $model.result -}}
|
||||
<div class="flex flex-row" id="contents-header-data">
|
||||
<div class="flex flex-col justify-end gap-y-6 pr-20">
|
||||
<div class="">
|
||||
<div class="font-bold text-sm">
|
||||
<i class="ri-navigation-line"></i> Navigation
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
{{- if $model.result.PrevByTitle -}}
|
||||
<tool-tip position="top" class="!inline">
|
||||
<div class="data-tip">{{ $model.result.PrevByTitle.PreferredTitle }}</div>
|
||||
<a
|
||||
href="/almanach/{{ $model.result.PrevByTitle.MusenalmID }}/contents/edit/"
|
||||
class="text-gray-700 hover:text-slate-950 no-underline">
|
||||
<i class="ri-arrow-left-s-line"></i>
|
||||
</a>
|
||||
</tool-tip>
|
||||
{{- else -}}
|
||||
<span class="text-gray-700 opacity-0 pointer-events-none">
|
||||
<i class="ri-arrow-left-s-line"></i>
|
||||
</span>
|
||||
{{- end -}}
|
||||
<span class="text-gray-800 font-bold no-underline">
|
||||
A - Z
|
||||
</span>
|
||||
{{- if $model.result.NextByTitle -}}
|
||||
<tool-tip position="top" class="!inline">
|
||||
<div class="data-tip">{{ $model.result.NextByTitle.PreferredTitle }}</div>
|
||||
<a
|
||||
href="/almanach/{{ $model.result.NextByTitle.MusenalmID }}/contents/edit/"
|
||||
class="text-gray-700 hover:text-slate-950 no-underline">
|
||||
<i class="ri-arrow-right-s-line"></i>
|
||||
</a>
|
||||
</tool-tip>
|
||||
{{- else -}}
|
||||
<span class="text-gray-700 opacity-0 pointer-events-none">
|
||||
<i class="ri-arrow-right-s-line"></i>
|
||||
</span>
|
||||
{{- end -}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col justify-end gap-y-6 pr-10 w-40 shrink-0">
|
||||
<div class="">
|
||||
<div class="font-bold text-sm">
|
||||
<i class="ri-database-2-line"></i> Datenbank-ID
|
||||
</div>
|
||||
<div class="whitespace-nowrap tabular-nums">{{ $model.result.Entry.Id }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col justify-end gap-y-6 pr-4 w-48 shrink-0">
|
||||
<div class="">
|
||||
<div class="font-bold text-sm mb-1"><i class="ri-calendar-line"></i> Zuletzt bearbeitet</div>
|
||||
<div>
|
||||
<div class="px-1.5 py-0.5 rounded-xs bg-gray-200 w-fit whitespace-nowrap tabular-nums">
|
||||
<span>{{ GermanDate $model.result.Entry.Updated }}</span>,
|
||||
<span>{{ GermanTime $model.result.Entry.Updated }}</span>h
|
||||
</div>
|
||||
<div
|
||||
class="px-1.5 py-0.5 rounded-xs mt-1.5 bg-gray-200 w-fit {{ if not $model.result.User }}hidden{{ end }}">
|
||||
<i class="ri-user-line mr-1"></i>
|
||||
<span>{{- if $model.result.User -}}{{ $model.result.User.Name }}{{- end -}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{- end -}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container-normal mx-auto mt-1 !px-0">
|
||||
{{ template "_usermessage" $model }}
|
||||
<div id="contents-sync-indicator" class="fixed right-6 bottom-6 z-50 hidden rounded-full bg-stone-200 px-3 py-2 text-sm text-stone-700 shadow-md">
|
||||
<i class="ri-loader-4-line spinning mr-2"></i>
|
||||
Reihenfolge wird gespeichert
|
||||
</div>
|
||||
<input type="hidden" name="csrf_token" value="{{ $model.csrf_token }}" data-role="csrf-token" />
|
||||
<div class="flex items-center gap-3 px-4 hidden">
|
||||
<form
|
||||
method="POST"
|
||||
action="/almanach/{{ $model.result.Entry.MusenalmID }}/contents/edit/extent"
|
||||
class="extent-inline flex-1 grid grid-cols-[max-content_minmax(12rem,1fr)_max-content] items-center gap-2">
|
||||
<input type="hidden" name="csrf_token" value="{{ $model.csrf_token }}" />
|
||||
<label for="contents-extent" class="text-base font-bold text-gray-700">Struktur</label>
|
||||
<input
|
||||
id="contents-extent"
|
||||
name="extent"
|
||||
type="text"
|
||||
class="min-w-[10rem] w-full max-w-none flex-1 border border-slate-300 rounded-xs bg-white px-3 py-1.5 text-base leading-6 text-gray-800 focus:outline-none focus:ring-2 focus:ring-slate-400/30"
|
||||
placeholder="z. B. 12 Bl., 3 Taf."
|
||||
value="{{- $model.result.Entry.Extent -}}" />
|
||||
<button type="submit" class="rounded-xs border border-slate-300 bg-stone-50 px-3 py-1.5 text-base font-semibold text-gray-700 shadow-sm hover:bg-stone-100">
|
||||
Struktur speichern
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="px-4 mt-1 flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-2 text-lg font-bold text-gray-700">
|
||||
<i class="ri-folder-open-line"></i>
|
||||
<span>Inhalt</span>
|
||||
</div>
|
||||
<span id="content-filter-count" class="text-sm font-semibold text-gray-600 whitespace-nowrap" data-role="content-filter-count">
|
||||
{{ len $model.result.Contents }} Einträge
|
||||
</span>
|
||||
</div>
|
||||
{{- $hasContents := gt (len $model.result.Contents) 0 -}}
|
||||
{{- if $hasContents -}}
|
||||
<hr class="mx-4 mt-2 mb-1 border-slate-300" />
|
||||
<div class="px-4 py-2 flex flex-wrap items-center gap-3">
|
||||
<input
|
||||
id="content-filter"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
class="flex-1 min-w-48 border border-slate-300 rounded-xs bg-white px-3 py-1.5 text-base leading-6 text-gray-800 focus:outline-none focus:ring-2 focus:ring-slate-400/30"
|
||||
placeholder="Suche nach Titel, Person, Anmerkung..."
|
||||
data-role="content-filter-input"
|
||||
/>
|
||||
<input
|
||||
id="content-filter-page"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
class="w-28 border border-slate-300 rounded-xs bg-white px-3 py-1.5 text-base leading-6 text-gray-800 focus:outline-none focus:ring-2 focus:ring-slate-400/30"
|
||||
placeholder="Seite"
|
||||
data-role="content-filter-page"
|
||||
/>
|
||||
<select
|
||||
id="content-filter-type"
|
||||
class="border border-slate-300 rounded-xs bg-white px-3 py-1.5 text-base leading-6 text-gray-800 focus:outline-none focus:ring-2 focus:ring-slate-400/30"
|
||||
data-role="content-filter-type">
|
||||
<option value="">Alle Typen</option>
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
class="content-action-button"
|
||||
onclick="window.location.assign('/almanach/{{ $model.result.Entry.MusenalmID }}/contents/new')">
|
||||
<i class="ri-add-line"></i>
|
||||
<span>Neuer Beitrag</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mx-4 mt-2 rounded-md border border-slate-300 bg-white flex flex-col overflow-hidden p-1" data-role="contents-panel">
|
||||
<div class="flex-1 min-h-0 overflow-y-scroll">
|
||||
<div class="flex flex-col gap-0"
|
||||
data-role="contents-list"
|
||||
data-order-endpoint="/almanach/{{ $model.result.Entry.MusenalmID }}/contents/edit">
|
||||
{{- range $_, $content := $model.result.Contents -}}
|
||||
{{- template "_content_item" (Dict
|
||||
"content" $content
|
||||
"entry" $model.result.Entry
|
||||
"csrf_token" $model.csrf_token
|
||||
"content_types" $model.content_types
|
||||
"musenalm_types" $model.musenalm_types
|
||||
"pagination_values" $model.pagination_values
|
||||
"agents" $model.result.Agents
|
||||
"content_agents" (index $model.result.ContentsAgents $content.Id)
|
||||
"agent_relations" $model.agent_relations
|
||||
"open_edit" false
|
||||
"is_new" false
|
||||
) -}}
|
||||
{{- end -}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{- else -}}
|
||||
<div class="mx-4 mt-2">
|
||||
<div class="min-h-[18rem] flex items-center justify-center text-center px-6 py-12">
|
||||
<div class="text-lg text-gray-700 font-semibold flex flex-col items-center gap-4">
|
||||
<i class="ri-information-line text-3xl text-gray-600"></i>
|
||||
<span>Keine Beiträge verzeichnet.</span>
|
||||
<button
|
||||
type="button"
|
||||
class="content-action-button"
|
||||
onclick="window.location.assign('/almanach/{{ $model.result.Entry.MusenalmID }}/contents/new')">
|
||||
<i class="ri-add-line"></i>
|
||||
<span>Neuen Beitrag anlegen</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{- end -}}
|
||||
</div>
|
||||
|
||||
<dialog id="content-delete-dialog" class="dbform fixed inset-0 m-auto rounded-md border border-slate-200 p-0 shadow-xl backdrop:bg-black/40">
|
||||
<div class="p-5 w-[22rem]">
|
||||
<div class="text-base font-bold text-gray-900">Eintrag löschen?</div>
|
||||
<div id="content-delete-dialog-title" class="text-sm font-bold text-gray-900 mt-1"></div>
|
||||
<p class="text-sm text-gray-700 mt-2">
|
||||
Der Eintrag wird dauerhaft gelöscht. Verknüpfungen, Exemplare und Beiträge werden entfernt.
|
||||
</p>
|
||||
<div class="flex items-center justify-end gap-3 mt-4">
|
||||
<button type="button" class="resetbutton w-auto px-3 py-1 text-sm" data-role="content-delete-cancel">Abbrechen</button>
|
||||
<button type="button" class="submitbutton w-auto bg-red-700 hover:bg-red-800 px-3 py-1 text-sm" data-role="content-delete-confirm">
|
||||
Löschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
</edit-page>
|
||||
|
||||
<script>
|
||||
// Search index: object keyed by content ID with all searchable fields
|
||||
window.contentsSearchIndex = {
|
||||
{{- range $i, $content := $model.result.Contents -}}
|
||||
{{- if $i }},{{ end }}
|
||||
"{{ $content.Id }}": {
|
||||
extent: "{{ $content.Extent }}",
|
||||
musenalmType: [{{- range $j, $t := $content.MusenalmType -}}{{- if $j }},{{ end }}"{{ $t }}"{{- end -}}],
|
||||
preferredTitle: "{{ $content.PreferredTitle }}",
|
||||
titleStmt: "{{ $content.TitleStmt }}",
|
||||
status: "{{ $content.EditState }}",
|
||||
subtitleStmt: "{{ $content.SubtitleStmt }}",
|
||||
incipitStmt: "{{ $content.IncipitStmt }}",
|
||||
responsibilityStmt: "{{ $content.ResponsibilityStmt }}",
|
||||
parallelTitle: "{{ $content.ParallelTitle }}",
|
||||
variantTitle: "{{ $content.VariantTitle }}",
|
||||
placeStmt: "{{ $content.PlaceStmt }}",
|
||||
language: [{{- range $j, $lang := $content.Language -}}{{- if $j }},{{ end }}"{{ $lang }}"{{- end -}}],
|
||||
contentType: [{{- range $j, $ct := $content.ContentType -}}{{- if $j }},{{ end }}"{{ $ct }}"{{- end -}}],
|
||||
comment: "{{ $content.Comment }}",
|
||||
agents: [
|
||||
{{- $contentAgents := index $model.result.ContentsAgents $content.Id -}}
|
||||
{{- range $j, $rel := $contentAgents -}}
|
||||
{{- $agent := index $model.result.Agents $rel.Agent -}}
|
||||
{{- if $agent -}}
|
||||
{{- if $j }},{{ end }}{name: "{{ $agent.Name }}", bio: "{{ $agent.BiographicalData }}"}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
],
|
||||
annotation: {{ SafeJS $content.Annotation }}
|
||||
}
|
||||
{{- end -}}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
let list = null;
|
||||
|
||||
// Normalize function for accent-insensitive matching
|
||||
const normalizeText = (text) => {
|
||||
if (!text) return '';
|
||||
return text
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '') // Remove diacritics
|
||||
.trim();
|
||||
};
|
||||
|
||||
// Filter functionality
|
||||
const initFilter = () => {
|
||||
const filterInput = document.querySelector("[data-role='content-filter-input']");
|
||||
const filterPage = document.querySelector("[data-role='content-filter-page']");
|
||||
const filterType = document.querySelector("[data-role='content-filter-type']");
|
||||
const filterCount = document.querySelector("[data-role='content-filter-count']");
|
||||
const list = document.querySelector("[data-role='contents-list']");
|
||||
|
||||
if (!filterInput || !list) return;
|
||||
if (filterInput.dataset.init === "true") return;
|
||||
filterInput.dataset.init = "true";
|
||||
|
||||
// Populate type select from search index
|
||||
if (filterType && window.contentsSearchIndex) {
|
||||
const types = new Set();
|
||||
Object.values(window.contentsSearchIndex).forEach(content => {
|
||||
(content.musenalmType || []).forEach(t => types.add(t));
|
||||
});
|
||||
Array.from(types).sort().forEach(type => {
|
||||
const option = document.createElement('option');
|
||||
option.value = type;
|
||||
option.textContent = type;
|
||||
filterType.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
let filterTimeout = null;
|
||||
const totalCount = list.querySelectorAll("[data-role='content-item']").length;
|
||||
|
||||
// Field labels for display
|
||||
const fieldLabels = {
|
||||
extent: 'Seite',
|
||||
musenalmType: 'Typ',
|
||||
titleStmt: 'Titel',
|
||||
subtitleStmt: 'Untertitel',
|
||||
incipitStmt: 'Incipit',
|
||||
responsibilityStmt: 'Autorangabe',
|
||||
parallelTitle: 'Paralleltitel',
|
||||
variantTitle: 'Titelvarianten',
|
||||
placeStmt: 'Ortsangabe',
|
||||
contentType: 'Beitragstyp',
|
||||
comment: 'Kommentar',
|
||||
agents: 'Personen',
|
||||
annotation: 'Anmerkung'
|
||||
};
|
||||
|
||||
const getMarkTargets = () => {
|
||||
if (!list) {
|
||||
return [];
|
||||
}
|
||||
const visibleItems = Array.from(list.querySelectorAll("[data-role='content-item']")).filter((item) => item.style.display !== "none");
|
||||
const targets = [];
|
||||
visibleItems.forEach((item) => {
|
||||
targets.push(...item.querySelectorAll(".content-search-text"));
|
||||
});
|
||||
return targets;
|
||||
};
|
||||
|
||||
const updateMarks = () => {
|
||||
if (typeof Mark !== "function") {
|
||||
return;
|
||||
}
|
||||
const terms = [];
|
||||
const rawQuery = (filterInput.value || "").trim();
|
||||
const rawPage = (filterPage?.value || "").trim();
|
||||
const rawType = (filterType?.value || "").trim();
|
||||
if (rawQuery) {
|
||||
terms.push(rawQuery);
|
||||
}
|
||||
if (rawPage) {
|
||||
terms.push(rawPage);
|
||||
}
|
||||
if (rawType) {
|
||||
terms.push(rawType);
|
||||
}
|
||||
const targets = getMarkTargets();
|
||||
if (targets.length === 0) {
|
||||
return;
|
||||
}
|
||||
const markInstance = new Mark(targets);
|
||||
markInstance.unmark({
|
||||
done: () => {
|
||||
if (terms.length > 0) {
|
||||
markInstance.mark(terms, {
|
||||
separateWordSearch: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const performFilter = () => {
|
||||
const query = normalizeText(filterInput.value);
|
||||
const pageQuery = normalizeText(filterPage?.value || '');
|
||||
const typeQuery = filterType?.value || '';
|
||||
const items = list.querySelectorAll("[data-role='content-item']");
|
||||
let visibleCount = 0;
|
||||
|
||||
const hasAnyFilter = query || pageQuery || typeQuery;
|
||||
|
||||
if (!hasAnyFilter) {
|
||||
list.removeAttribute('data-filtering');
|
||||
items.forEach(item => {
|
||||
item.style.display = '';
|
||||
const matchDisplay = item.querySelector('[data-role="content-match-display"]');
|
||||
if (matchDisplay) {
|
||||
matchDisplay.classList.add('hidden');
|
||||
matchDisplay.innerHTML = '';
|
||||
}
|
||||
});
|
||||
visibleCount = totalCount;
|
||||
} else {
|
||||
list.setAttribute('data-filtering', 'true');
|
||||
items.forEach(item => {
|
||||
const contentId = item.dataset.contentId;
|
||||
const contentData = window.contentsSearchIndex[contentId];
|
||||
const matchDisplay = item.querySelector('[data-role="content-match-display"]');
|
||||
|
||||
if (!contentData) {
|
||||
item.style.display = 'none';
|
||||
if (matchDisplay) {
|
||||
matchDisplay.classList.add('hidden');
|
||||
matchDisplay.innerHTML = '';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check type filter (exact match)
|
||||
if (typeQuery && !(contentData.musenalmType || []).includes(typeQuery)) {
|
||||
item.style.display = 'none';
|
||||
if (matchDisplay) {
|
||||
matchDisplay.classList.add('hidden');
|
||||
matchDisplay.innerHTML = '';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check page filter (substring match on extent)
|
||||
if (pageQuery) {
|
||||
const extentMatch = normalizeText(contentData.extent || '').includes(pageQuery);
|
||||
if (!extentMatch) {
|
||||
item.style.display = 'none';
|
||||
if (matchDisplay) {
|
||||
matchDisplay.classList.add('hidden');
|
||||
matchDisplay.innerHTML = '';
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If only type/page filters are active (no text query), show item without match display
|
||||
if (!query) {
|
||||
item.style.display = '';
|
||||
if (matchDisplay) {
|
||||
matchDisplay.classList.add('hidden');
|
||||
matchDisplay.innerHTML = '';
|
||||
}
|
||||
visibleCount++;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check each field individually and collect matches for text query
|
||||
const matchedFields = [];
|
||||
|
||||
const checkField = (key, value, label) => {
|
||||
if (!value) return;
|
||||
const normalizedValue = normalizeText(String(value));
|
||||
if (normalizedValue.includes(query)) {
|
||||
matchedFields.push({ label, value: String(value) });
|
||||
}
|
||||
};
|
||||
|
||||
const checkArrayField = (key, values, label) => {
|
||||
if (!values || !values.length) return;
|
||||
const matchingValues = values.filter(v => normalizeText(String(v)).includes(query));
|
||||
if (matchingValues.length > 0) {
|
||||
matchedFields.push({ label, value: matchingValues.join(', ') });
|
||||
}
|
||||
};
|
||||
|
||||
// Check text search fields (excluding language, status, preferredTitle)
|
||||
checkArrayField('musenalmType', contentData.musenalmType, fieldLabels.musenalmType);
|
||||
checkField('titleStmt', contentData.titleStmt, fieldLabels.titleStmt);
|
||||
checkField('subtitleStmt', contentData.subtitleStmt, fieldLabels.subtitleStmt);
|
||||
checkField('incipitStmt', contentData.incipitStmt, fieldLabels.incipitStmt);
|
||||
checkField('responsibilityStmt', contentData.responsibilityStmt, fieldLabels.responsibilityStmt);
|
||||
checkField('parallelTitle', contentData.parallelTitle, fieldLabels.parallelTitle);
|
||||
checkField('variantTitle', contentData.variantTitle, fieldLabels.variantTitle);
|
||||
checkField('placeStmt', contentData.placeStmt, fieldLabels.placeStmt);
|
||||
checkArrayField('contentType', contentData.contentType, fieldLabels.contentType);
|
||||
checkField('comment', contentData.comment, fieldLabels.comment);
|
||||
|
||||
// Check agents
|
||||
if (contentData.agents && contentData.agents.length > 0) {
|
||||
const matchingAgents = contentData.agents.filter(a => {
|
||||
const agentText = normalizeText(`${a.name} ${a.bio}`);
|
||||
return agentText.includes(query);
|
||||
});
|
||||
if (matchingAgents.length > 0) {
|
||||
matchedFields.push({
|
||||
label: fieldLabels.agents,
|
||||
value: matchingAgents.map(a => a.bio ? `${a.name} (${a.bio})` : a.name).join(', ')
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check annotation (strip HTML for display)
|
||||
if (contentData.annotation) {
|
||||
const plainAnnotation = contentData.annotation.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
||||
if (normalizeText(plainAnnotation).includes(query)) {
|
||||
const displayValue = plainAnnotation.length > 100
|
||||
? plainAnnotation.substring(0, 100) + '…'
|
||||
: plainAnnotation;
|
||||
matchedFields.push({ label: fieldLabels.annotation, value: displayValue });
|
||||
}
|
||||
}
|
||||
|
||||
const hasMatch = matchedFields.length > 0;
|
||||
item.style.display = hasMatch ? '' : 'none';
|
||||
|
||||
if (matchDisplay) {
|
||||
if (hasMatch) {
|
||||
matchDisplay.classList.remove('hidden');
|
||||
matchDisplay.innerHTML = matchedFields
|
||||
.map(f => `<span class="inline-block mr-3"><strong>${f.label}:</strong> ${f.value}</span>`)
|
||||
.join('');
|
||||
} else {
|
||||
matchDisplay.classList.add('hidden');
|
||||
matchDisplay.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
if (hasMatch) visibleCount++;
|
||||
});
|
||||
}
|
||||
|
||||
if (filterCount) {
|
||||
filterCount.textContent = !hasAnyFilter
|
||||
? `${totalCount} Einträge`
|
||||
: `${visibleCount} von ${totalCount} Einträgen`;
|
||||
}
|
||||
|
||||
updateMarks();
|
||||
};
|
||||
|
||||
const debounceFilter = () => {
|
||||
if (filterTimeout) clearTimeout(filterTimeout);
|
||||
filterTimeout = setTimeout(performFilter, 150);
|
||||
};
|
||||
|
||||
filterInput.addEventListener('input', debounceFilter);
|
||||
filterPage?.addEventListener('input', debounceFilter);
|
||||
filterType?.addEventListener('change', performFilter);
|
||||
|
||||
// Clear all filters on Escape
|
||||
const clearFilters = () => {
|
||||
filterInput.value = '';
|
||||
if (filterPage) filterPage.value = '';
|
||||
if (filterType) filterType.value = '';
|
||||
performFilter();
|
||||
};
|
||||
|
||||
filterInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
clearFilters();
|
||||
filterInput.blur();
|
||||
}
|
||||
});
|
||||
|
||||
filterPage?.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
clearFilters();
|
||||
filterPage.blur();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const setContentsPanelHeight = () => {
|
||||
const panel = document.querySelector("[data-role='contents-panel']");
|
||||
if (!panel) {
|
||||
return;
|
||||
}
|
||||
const rect = panel.getBoundingClientRect();
|
||||
const viewportHeight = window.innerHeight || document.documentElement.clientHeight || 0;
|
||||
const bottomGap = 16;
|
||||
const available = Math.max(200, viewportHeight - rect.top - bottomGap);
|
||||
panel.style.height = `${available}px`;
|
||||
};
|
||||
|
||||
const initPage = () => {
|
||||
list = document.querySelector("[data-role='contents-list']");
|
||||
if (!list) {
|
||||
return;
|
||||
}
|
||||
|
||||
setContentsPanelHeight();
|
||||
|
||||
const getItems = () => Array.from(list.querySelectorAll("[data-role='content-item']"));
|
||||
if (getItems().length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const orderEndpoint = list.dataset.orderEndpoint || getItems()[0]?.querySelector("form")?.getAttribute("action") || "";
|
||||
const deleteEndpoint = window.location.pathname.replace(/\/contents\/edit\/?$/, "/contents/delete");
|
||||
const csrfToken = document.querySelector("input[name='csrf_token']")?.value || "";
|
||||
const syncIndicator = document.querySelector("#contents-sync-indicator");
|
||||
let orderSyncTimer = null;
|
||||
let isOrderSyncing = false;
|
||||
let pendingOrderSync = false;
|
||||
if (list.__contentsDragController) {
|
||||
list.__contentsDragController.abort();
|
||||
}
|
||||
list.__contentsDragController = new AbortController();
|
||||
const { signal: dragSignal } = list.__contentsDragController;
|
||||
|
||||
const setSyncIndicator = (active) => {
|
||||
if (!syncIndicator) {
|
||||
return;
|
||||
}
|
||||
syncIndicator.classList.toggle("hidden", !active);
|
||||
};
|
||||
const setDraggingState = (active) => {
|
||||
if (active) {
|
||||
document.body.dataset.dragging = "true";
|
||||
} else {
|
||||
delete document.body.dataset.dragging;
|
||||
}
|
||||
window.__toolTipDragging = active;
|
||||
window.dispatchEvent(new CustomEvent("contentsdragging", { detail: { active } }));
|
||||
};
|
||||
// Shared delete dialog
|
||||
const deleteDialog = document.getElementById("content-delete-dialog");
|
||||
const deleteDialogTitle = document.getElementById("content-delete-dialog-title");
|
||||
const deleteConfirmBtn = deleteDialog?.querySelector("[data-role='content-delete-confirm']");
|
||||
const deleteCancelBtn = deleteDialog?.querySelector("[data-role='content-delete-cancel']");
|
||||
let itemToDelete = null;
|
||||
|
||||
const deleteContent = (item) => {
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
const contentId = item.dataset.contentId || "";
|
||||
if (!contentId || !csrfToken) {
|
||||
return;
|
||||
}
|
||||
if (window.htmx?.ajax) {
|
||||
window.htmx.ajax("POST", deleteEndpoint, {
|
||||
target: item,
|
||||
swap: "outerHTML",
|
||||
values: {
|
||||
csrf_token: csrfToken,
|
||||
content_id: contentId,
|
||||
},
|
||||
});
|
||||
deleteDialog?.close();
|
||||
return;
|
||||
}
|
||||
const payload = new URLSearchParams();
|
||||
payload.set("csrf_token", csrfToken);
|
||||
payload.set("content_id", contentId);
|
||||
fetch(deleteEndpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"HX-Request": "true",
|
||||
},
|
||||
body: payload.toString(),
|
||||
})
|
||||
.then(() => {
|
||||
removeItem(item);
|
||||
})
|
||||
.catch(() => null)
|
||||
.finally(() => {
|
||||
deleteDialog?.close();
|
||||
});
|
||||
};
|
||||
|
||||
// Setup shared delete dialog events
|
||||
if (deleteDialog && deleteDialog.dataset.init !== "true") {
|
||||
deleteDialog.dataset.init = "true";
|
||||
deleteCancelBtn?.addEventListener("click", () => deleteDialog.close());
|
||||
deleteDialog.addEventListener("cancel", (e) => {
|
||||
e.preventDefault();
|
||||
deleteDialog.close();
|
||||
});
|
||||
deleteConfirmBtn?.addEventListener("click", () => {
|
||||
if (itemToDelete) {
|
||||
deleteContent(itemToDelete);
|
||||
itemToDelete = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const performOrderSync = () => {
|
||||
if (!list || !orderEndpoint || !csrfToken || isOrderSyncing) {
|
||||
pendingOrderSync = true;
|
||||
return;
|
||||
}
|
||||
isOrderSyncing = true;
|
||||
pendingOrderSync = false;
|
||||
setSyncIndicator(true);
|
||||
const payload = new URLSearchParams();
|
||||
payload.set("csrf_token", csrfToken);
|
||||
list.querySelectorAll("[data-role='content-item']").forEach((card) => {
|
||||
// Skip filtered-out items during reorder
|
||||
if (card.style.display === 'none') return;
|
||||
|
||||
const contentId = card.dataset.contentId;
|
||||
if (!contentId) {
|
||||
return;
|
||||
}
|
||||
payload.append("content_order[]", contentId);
|
||||
});
|
||||
fetch(orderEndpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: payload.toString(),
|
||||
})
|
||||
.catch(() => null)
|
||||
.finally(() => {
|
||||
isOrderSyncing = false;
|
||||
setSyncIndicator(false);
|
||||
if (pendingOrderSync) {
|
||||
performOrderSync();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const syncOrder = () => {
|
||||
if (orderSyncTimer) {
|
||||
clearTimeout(orderSyncTimer);
|
||||
}
|
||||
orderSyncTimer = setTimeout(() => {
|
||||
performOrderSync();
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const removeItem = (item) => {
|
||||
item.remove();
|
||||
};
|
||||
|
||||
const setupItem = (item) => {
|
||||
if (!item || item.dataset.init === "true") {
|
||||
return;
|
||||
}
|
||||
item.dataset.init = "true";
|
||||
|
||||
const deleteButtonView = item.querySelector("[data-role='content-delete-view']");
|
||||
|
||||
if (deleteButtonView && deleteDialog) {
|
||||
deleteButtonView.addEventListener("click", () => {
|
||||
itemToDelete = item;
|
||||
// Get title from search index for the dialog
|
||||
const contentId = item.dataset.contentId;
|
||||
const contentData = window.contentsSearchIndex?.[contentId];
|
||||
const title = contentData?.titleStmt || contentData?.preferredTitle || '';
|
||||
if (deleteDialogTitle) {
|
||||
deleteDialogTitle.textContent = title;
|
||||
deleteDialogTitle.style.display = title ? '' : 'none';
|
||||
}
|
||||
deleteDialog.showModal();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
getItems().forEach((item) => {
|
||||
setupItem(item);
|
||||
});
|
||||
|
||||
list.dataset.pageInit = "true";
|
||||
let draggedItem = null;
|
||||
let pointerDrag = null;
|
||||
let lastDragOverTime = 0;
|
||||
const DRAG_THROTTLE_MS = 100;
|
||||
const startPointerDrag = (event) => {
|
||||
const handle = event.target.closest("[data-role='content-drag-handle']");
|
||||
if (!handle || event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
const item = handle.closest("[data-role='content-item']");
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
pointerDrag = {
|
||||
item,
|
||||
pointerId: event.pointerId,
|
||||
};
|
||||
item.dataset.dragging = "true";
|
||||
item.classList.add("opacity-60");
|
||||
setDraggingState(true);
|
||||
if (handle.setPointerCapture) {
|
||||
handle.setPointerCapture(event.pointerId);
|
||||
}
|
||||
};
|
||||
|
||||
const movePointerDrag = (event) => {
|
||||
if (!pointerDrag || event.pointerId !== pointerDrag.pointerId) {
|
||||
return;
|
||||
}
|
||||
const now = Date.now();
|
||||
if (now - lastDragOverTime < DRAG_THROTTLE_MS) {
|
||||
return;
|
||||
}
|
||||
lastDragOverTime = now;
|
||||
const item = pointerDrag.item;
|
||||
const targetItem = document.elementFromPoint(event.clientX, event.clientY)?.closest("[data-role='content-item']");
|
||||
if (!targetItem || targetItem === item) {
|
||||
return;
|
||||
}
|
||||
const rect = targetItem.getBoundingClientRect();
|
||||
const before = event.clientY - rect.top < rect.height / 2;
|
||||
if (before) {
|
||||
targetItem.before(item);
|
||||
} else {
|
||||
targetItem.after(item);
|
||||
}
|
||||
};
|
||||
|
||||
const endPointerDrag = (event) => {
|
||||
if (!pointerDrag || event.pointerId !== pointerDrag.pointerId) {
|
||||
return;
|
||||
}
|
||||
pointerDrag.item.classList.remove("opacity-60");
|
||||
pointerDrag.item.dataset.dragging = "";
|
||||
pointerDrag = null;
|
||||
setDraggingState(false);
|
||||
syncOrder();
|
||||
};
|
||||
list.addEventListener("click", (event) => {
|
||||
const moveUp = event.target.closest("[data-role='content-move-up']");
|
||||
const moveDown = event.target.closest("[data-role='content-move-down']");
|
||||
if (!moveUp && !moveDown) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
const item = event.target.closest("[data-role='content-item']");
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
if (moveUp) {
|
||||
let prev = item.previousElementSibling;
|
||||
while (prev && !prev.matches("[data-role='content-item']")) {
|
||||
prev = prev.previousElementSibling;
|
||||
}
|
||||
if (prev) {
|
||||
prev.before(item);
|
||||
}
|
||||
} else {
|
||||
let next = item.nextElementSibling;
|
||||
while (next && !next.matches("[data-role='content-item']")) {
|
||||
next = next.nextElementSibling;
|
||||
}
|
||||
if (next) {
|
||||
next.after(item);
|
||||
}
|
||||
}
|
||||
syncOrder();
|
||||
}, { signal: dragSignal });
|
||||
|
||||
list.addEventListener("dragstart", (event) => {
|
||||
if (pointerDrag) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (event.target.closest("[data-role='content-move-up']") || event.target.closest("[data-role='content-move-down']")) {
|
||||
return;
|
||||
}
|
||||
if (event.target.closest(".status-badge") || event.target.closest("multi-select-simple") || event.target.closest("select")) {
|
||||
return;
|
||||
}
|
||||
const handle = event.target.closest("[data-role='content-drag-handle']");
|
||||
if (!handle) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
const item = handle.closest("[data-role='content-item']");
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
draggedItem = item;
|
||||
item.dataset.dragging = "true";
|
||||
draggedItem.classList.add("opacity-60");
|
||||
setDraggingState(true);
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
event.dataTransfer.setData("text/plain", "move");
|
||||
}, { signal: dragSignal });
|
||||
|
||||
list.addEventListener("dragover", (event) => {
|
||||
if (!draggedItem) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
const targetItem = event.target.closest("[data-role='content-item']");
|
||||
if (!targetItem || targetItem === draggedItem) {
|
||||
return;
|
||||
}
|
||||
const rect = targetItem.getBoundingClientRect();
|
||||
const before = event.clientY - rect.top < rect.height / 2;
|
||||
if (before) {
|
||||
targetItem.before(draggedItem);
|
||||
} else {
|
||||
targetItem.after(draggedItem);
|
||||
}
|
||||
}, { signal: dragSignal });
|
||||
|
||||
list.addEventListener("dragend", () => {
|
||||
if (draggedItem) {
|
||||
draggedItem.classList.remove("opacity-60");
|
||||
draggedItem.dataset.dragging = "";
|
||||
}
|
||||
draggedItem = null;
|
||||
setDraggingState(false);
|
||||
syncOrder();
|
||||
}, { signal: dragSignal });
|
||||
|
||||
list.addEventListener("pointerdown", startPointerDrag, { signal: dragSignal });
|
||||
list.addEventListener("pointermove", movePointerDrag, { signal: dragSignal });
|
||||
list.addEventListener("pointerup", endPointerDrag, { signal: dragSignal });
|
||||
list.addEventListener("pointercancel", endPointerDrag, { signal: dragSignal });
|
||||
};
|
||||
|
||||
initFilter();
|
||||
initPage();
|
||||
window.addEventListener("pageshow", (event) => {
|
||||
if (event.persisted) {
|
||||
initFilter();
|
||||
initPage();
|
||||
}
|
||||
});
|
||||
if (!window.__contentsPanelResizeBound) {
|
||||
window.__contentsPanelResizeBound = true;
|
||||
window.addEventListener("resize", () => {
|
||||
setContentsPanelHeight();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -1,8 +0,0 @@
|
||||
{{ $model := . }}
|
||||
<title>
|
||||
{{ if $model.result }}
|
||||
Beiträge bearbeiten: {{ $model.result.Entry.PreferredTitle }} - Musenalm
|
||||
{{ else }}
|
||||
Beiträge bearbeiten - Musenalm
|
||||
{{ end }}
|
||||
</title>
|
||||
@@ -1,521 +0,0 @@
|
||||
{{ $model := . }}
|
||||
|
||||
<edit-page>
|
||||
<div class="flex container-normal bg-slate-100 mx-auto px-8">
|
||||
<div class="flex flex-row w-full justify-between">
|
||||
<div class="flex flex-col justify-end-safe flex-2/5">
|
||||
<div class="mb-1">
|
||||
<div class="header-tabs">
|
||||
{{- if $model.result -}}
|
||||
<a href="/almanach/{{- $model.result.Entry.MusenalmID -}}/edit/" class="header-tab">
|
||||
<i class="ri-book-line"></i> Almanach
|
||||
</a>
|
||||
{{- else -}}
|
||||
<span class="header-tab header-tab-disabled">
|
||||
<i class="ri-book-line"></i> Almanach
|
||||
</span>
|
||||
{{- end -}}
|
||||
<a href="/almanach/{{- $model.result.Entry.MusenalmID -}}/contents/edit/" class="header-tab">
|
||||
<i class="ri-file-list-3-line"></i> Beiträge
|
||||
</a>
|
||||
<span class="header-tab-sep" aria-hidden="true">
|
||||
<i class="ri-arrow-right-s-line"></i>
|
||||
</span>
|
||||
<span class="header-tab header-tab-active" aria-current="page">
|
||||
<i class="ri-file-text-line"></i> Einzelbeitrag
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="text-2xl w-full font-bold text-slate-900 mb-1">
|
||||
{{- if $model.result -}}
|
||||
{{- $model.result.Entry.PreferredTitle -}}<br>
|
||||
{{- if $model.is_new -}}
|
||||
<span class="text-base font-semibold text-slate-700">Neuer Beitrag</span>
|
||||
{{- else -}}
|
||||
<span class="mt-1 mb-1 block text-base font-semibold text-slate-900">Beitrag Nr. {{ $model.content.MusenalmID }}</span>
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
Beiträge bearbeiten
|
||||
{{- end -}}
|
||||
</h1>
|
||||
{{- if $model.result -}}
|
||||
<div class="flex flex-row gap-x-3">
|
||||
{{- if not $model.is_new -}}
|
||||
<div>
|
||||
<a
|
||||
href="/beitrag/{{ $model.content.MusenalmID }}"
|
||||
class="text-gray-700 hover:text-slate-950 block no-underline">
|
||||
<i class="ri-eye-line"></i> Anschauen
|
||||
</a>
|
||||
</div>
|
||||
·
|
||||
<div class="flex flex-row">
|
||||
{{- if $model.prev_content -}}
|
||||
<div>
|
||||
<a href="/almanach/{{ $model.result.Entry.MusenalmID }}/contents/{{ $model.prev_content.MusenalmID }}/edit/" class="text-gray-700 hover:text-slate-950 no-underline block">
|
||||
<i class="ri-arrow-left-s-line"></i>
|
||||
</a>
|
||||
</div>
|
||||
{{- end -}}
|
||||
<div class="px-1.5 py-0.5 rounded-xs bg-gray-200 w-fit font-bold">
|
||||
{{ $model.content.MusenalmID }}
|
||||
</div>
|
||||
{{- if $model.next_content -}}
|
||||
<div>
|
||||
<a href="/almanach/{{ $model.result.Entry.MusenalmID }}/contents/{{ $model.next_content.MusenalmID }}/edit/" class="text-gray-700 hover:text-slate-950 no-underline block">
|
||||
<i class="ri-arrow-right-s-line"></i>
|
||||
</a>
|
||||
</div>
|
||||
{{- end -}}
|
||||
</div>
|
||||
{{- end -}}
|
||||
</div>
|
||||
{{- end -}}
|
||||
</div>
|
||||
{{- if $model.result -}}
|
||||
<div class="flex flex-row" id="contents-header-data">
|
||||
<div class="flex flex-col justify-end gap-y-6 pr-20">
|
||||
<div class="">
|
||||
<div class="font-bold text-sm">
|
||||
<i class="ri-navigation-line"></i> Navigation
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
{{- if not $model.is_new -}}
|
||||
{{- if $model.prev_content -}}
|
||||
<tool-tip position="top" class="!inline">
|
||||
<div class="data-tip">
|
||||
{{- if $model.prev_content.PreferredTitle -}}
|
||||
{{- $model.prev_content.PreferredTitle -}}
|
||||
{{- else if $model.prev_content.TitleStmt -}}
|
||||
{{- $model.prev_content.TitleStmt -}}
|
||||
{{- else -}}
|
||||
Beitrag Nr. {{ $model.prev_content.MusenalmID }}
|
||||
{{- end -}}
|
||||
</div>
|
||||
<a
|
||||
href="/almanach/{{ $model.result.Entry.MusenalmID }}/contents/{{ $model.prev_content.MusenalmID }}/edit/"
|
||||
class="text-gray-700 hover:text-slate-950 no-underline">
|
||||
<i class="ri-arrow-left-s-line"></i>
|
||||
</a>
|
||||
</tool-tip>
|
||||
{{- else -}}
|
||||
<span class="text-gray-700 opacity-0 pointer-events-none">
|
||||
<i class="ri-arrow-left-s-line"></i>
|
||||
</span>
|
||||
{{- end -}}
|
||||
<span class="text-gray-800 font-bold no-underline tabular-nums whitespace-nowrap">
|
||||
<span class="inline-block w-[2.5ch] text-right">{{ $model.content_index }}</span> / <span class="inline-block w-[2.5ch] text-left">{{ $model.content_total }}</span>
|
||||
</span>
|
||||
{{- if $model.next_content -}}
|
||||
<tool-tip position="top" class="!inline">
|
||||
<div class="data-tip">
|
||||
{{- if $model.next_content.PreferredTitle -}}
|
||||
{{- $model.next_content.PreferredTitle -}}
|
||||
{{- else if $model.next_content.TitleStmt -}}
|
||||
{{- $model.next_content.TitleStmt -}}
|
||||
{{- else -}}
|
||||
Beitrag Nr. {{ $model.next_content.MusenalmID }}
|
||||
{{- end -}}
|
||||
</div>
|
||||
<a
|
||||
href="/almanach/{{ $model.result.Entry.MusenalmID }}/contents/{{ $model.next_content.MusenalmID }}/edit/"
|
||||
class="text-gray-700 hover:text-slate-950 no-underline">
|
||||
<i class="ri-arrow-right-s-line"></i>
|
||||
</a>
|
||||
</tool-tip>
|
||||
{{- else -}}
|
||||
<span class="text-gray-700 opacity-0 pointer-events-none">
|
||||
<i class="ri-arrow-right-s-line"></i>
|
||||
</span>
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
<span class="text-gray-800 font-bold no-underline">Neu</span>
|
||||
{{- end -}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col justify-end gap-y-6 pr-10 w-40 shrink-0">
|
||||
<div class="">
|
||||
<div class="font-bold text-sm">
|
||||
<i class="ri-database-2-line"></i> Datenbank-ID
|
||||
</div>
|
||||
<div class="whitespace-nowrap tabular-nums">{{ $model.content.Id }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col justify-end gap-y-6 pr-4 w-48 shrink-0">
|
||||
<div class="">
|
||||
<div class="font-bold text-sm mb-1"><i class="ri-calendar-line"></i> Zuletzt bearbeitet</div>
|
||||
<div>
|
||||
<div class="px-1.5 py-0.5 rounded-xs bg-gray-200 w-fit whitespace-nowrap tabular-nums">
|
||||
<span>{{ GermanDate $model.result.Entry.Updated }}</span>,
|
||||
<span>{{ GermanTime $model.result.Entry.Updated }}</span>h
|
||||
</div>
|
||||
<div
|
||||
class="px-1.5 py-0.5 rounded-xs mt-1.5 bg-gray-200 w-fit {{ if not $model.result.User }}hidden{{ end }}">
|
||||
<i class="ri-user-line mr-1"></i>
|
||||
<span>{{- if $model.result.User -}}{{ $model.result.User.Name }}{{- end -}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{- end -}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container-normal mx-auto mt-4 !px-0">
|
||||
{{/* usermessage moved into action bar */}}
|
||||
<form
|
||||
autocomplete="off"
|
||||
class="w-full dbform form-with-action-bar"
|
||||
method="POST"
|
||||
enctype="multipart/form-data"
|
||||
hx-boost="false"
|
||||
action="/almanach/{{ $model.result.Entry.MusenalmID }}/contents/edit">
|
||||
<input type="hidden" name="csrf_token" value="{{ $model.csrf_token }}" />
|
||||
<input type="hidden" name="content_id" value="{{ $model.content_id }}" />
|
||||
|
||||
<div class="flex gap-8">
|
||||
<div class="flex-1 flex flex-col gap-4">
|
||||
{{- template "_content_edit" (Dict
|
||||
"content" $model.content
|
||||
"content_id" $model.content_id
|
||||
"entry" $model.result.Entry
|
||||
"content_types" $model.content_types
|
||||
"musenalm_types" $model.musenalm_types
|
||||
"pagination_values" $model.pagination_values
|
||||
"agents" $model.agents
|
||||
"content_agents" $model.content_agents
|
||||
"agent_relations" $model.agent_relations
|
||||
) -}}
|
||||
</div>
|
||||
|
||||
<div class="w-[28rem] shrink-0 flex flex-col gap-3">
|
||||
{{- $prefix := printf "content_%s_" $model.content_id -}}
|
||||
{{- $fieldId := printf "content-%s-edit-state" $model.content_id -}}
|
||||
{{ template "_content_status_edit" (Arr $model.content $prefix $fieldId) }}
|
||||
<div class="inputwrapper">
|
||||
<div class="inputlabelrow">
|
||||
<div class="flex items-center gap-1">
|
||||
<label for="content-{{ $model.content_id }}-musenalm-type" class="inputlabel">Beitragstyp(en)</label>
|
||||
<tool-tip position="top" class="!inline">
|
||||
<div class="data-tip">{{ helpOr "contents" "musenalm_type" "Musenalm-Typen des Beitrags." }}</div>
|
||||
<i class="ri-question-line"></i>
|
||||
</tool-tip>
|
||||
</div>
|
||||
<button type="button" id="content-{{ $model.content_id }}-musenalm-type-toggle" class="text-sm font-bold text-gray-700 hover:text-slate-950 no-underline pr-3">
|
||||
<i class="ri-link"></i> Typ verlinken
|
||||
</button>
|
||||
</div>
|
||||
<div class="px-2 py-2">
|
||||
<multi-select-simple
|
||||
id="content-{{ $model.content_id }}-musenalm-type"
|
||||
name="content_{{ $model.content_id }}_musenalm_type[]"
|
||||
data-external-toggle-id="content-{{ $model.content_id }}-musenalm-type-toggle"
|
||||
show-create-button="false"
|
||||
placeholder="Musenalm-Typen suchen..."
|
||||
data-toggle-label='<i class="ri-add-circle-line"></i>'
|
||||
data-empty-text="Keine Typen verknüpft"
|
||||
value='[{{- range $i, $t := $model.content.MusenalmType -}}{{- if $i }},{{ end -}}"{{ $t }}"{{- end -}}]'
|
||||
data-initial-options='[{{- range $i, $t := $model.musenalm_types -}}{{- if $i }},{{ end -}}{{ printf "{\"id\":%q,\"name\":%q}" $t $t }}{{- end -}}]'
|
||||
data-initial-values='[{{- range $i, $t := $model.content.MusenalmType -}}{{- if $i }},{{ end -}}{{ printf "%q" $t }}{{- end -}}]'>
|
||||
</multi-select-simple>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inputwrapper">
|
||||
<div class="inputlabelrow">
|
||||
<label for="content-{{ $model.content_id }}-language" class="inputlabel">Sprache</label>
|
||||
<button type="button" id="content-{{ $model.content_id }}-language-toggle" class="text-sm font-bold text-gray-700 hover:text-slate-950 no-underline pr-3">
|
||||
<i class="ri-link"></i> Sprache verlinken
|
||||
</button>
|
||||
</div>
|
||||
<div class="px-2 py-2">
|
||||
<multi-select-simple
|
||||
id="content-{{ $model.content_id }}-language"
|
||||
name="content_{{ $model.content_id }}_language[]"
|
||||
data-external-toggle-id="content-{{ $model.content_id }}-language-toggle"
|
||||
show-create-button="false"
|
||||
placeholder="Sprachen suchen..."
|
||||
data-toggle-label='<i class="ri-add-circle-line"></i>'
|
||||
data-empty-text="Keine Sprachen verknüpft"
|
||||
value='[{{- range $i, $lang := $model.content.Language -}}{{- if $i }},{{ end -}}"{{ $lang }}"{{- end -}}]'
|
||||
data-initial-values='[{{- range $i, $lang := $model.content.Language -}}{{- if $i }},{{ end -}}{{ printf "%q" $lang }}{{- end -}}]'>
|
||||
</multi-select-simple>
|
||||
</div>
|
||||
</div>
|
||||
{{- template "_content_images_panel" (Dict
|
||||
"content" $model.content
|
||||
"entry" $model.result.Entry
|
||||
"csrf_token" $model.csrf_token
|
||||
"is_new" false
|
||||
) -}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-action-bar">
|
||||
<div class="form-action-bar-inner">
|
||||
<p
|
||||
id="user-message"
|
||||
class="form-action-bar-message save-feedback {{ if $model.error }}save-feedback-error text-red-700{{ else if $model.success }}save-feedback-success text-green-700{{ else }}hidden{{ end }}"
|
||||
{{ if $model.success }}data-autohide="true"{{ end }}
|
||||
aria-live="polite">
|
||||
{{- if $model.error -}}
|
||||
{{ $model.error }}
|
||||
{{- else if $model.success -}}
|
||||
{{ $model.success }}
|
||||
{{- end -}}
|
||||
</p>
|
||||
<div class="form-action-bar-actions">
|
||||
<a href="/almanach/{{ $model.result.Entry.MusenalmID }}/contents/edit/" class="resetbutton w-40 flex items-center gap-2 justify-center">
|
||||
<i class="ri-arrow-left-line"></i>
|
||||
<span>Liste</span>
|
||||
</a>
|
||||
<a href="/almanach/{{ $model.result.Entry.MusenalmID }}/contents/new" class="resetbutton w-40 flex items-center gap-2 justify-center">
|
||||
<i class="ri-add-line"></i>
|
||||
<span>Neuer Beitrag</span>
|
||||
</a>
|
||||
{{- if not $model.is_new -}}
|
||||
<button type="button" class="resetbutton w-40 flex items-center gap-2 justify-center bg-red-50 text-red-800 hover:bg-red-100 hover:text-red-900" data-role="content-delete">
|
||||
<i class="ri-delete-bin-line"></i>
|
||||
<span>Eintrag loeschen</span>
|
||||
</button>
|
||||
{{- end -}}
|
||||
{{- if $model.content.MusenalmID -}}
|
||||
<button type="submit" name="save_action" value="stay" class="submitbutton flex items-center gap-2 justify-center">
|
||||
<i class="ri-save-line"></i>
|
||||
<span>Speichern</span>
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
name="save_action"
|
||||
value="view"
|
||||
class="submitbutton flex items-center gap-2 justify-center"
|
||||
>
|
||||
<i class="ri-eye-line"></i>
|
||||
<span>Speichern & Anzeigen</span>
|
||||
</button>
|
||||
{{- end -}}
|
||||
{{- if not $model.content.MusenalmID -}}
|
||||
<button type="submit" name="save_action" value="stay" class="submitbutton flex items-center gap-2 justify-center">
|
||||
<i class="ri-save-line"></i>
|
||||
<span>Speichern</span>
|
||||
</button>
|
||||
{{- end -}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<dialog data-role="content-delete-dialog" class="dbform fixed inset-0 m-auto rounded-md border border-slate-200 p-0 shadow-xl backdrop:bg-black/40">
|
||||
<div class="p-5 w-[22rem]">
|
||||
<div class="text-base font-bold text-gray-900">Eintrag loeschen?</div>
|
||||
{{- if $model.content.TitleStmt -}}
|
||||
<div class="text-sm font-bold text-gray-900 mt-1">{{ $model.content.TitleStmt }}</div>
|
||||
{{- end -}}
|
||||
<p class="text-sm text-gray-700 mt-2">
|
||||
Der Eintrag wird dauerhaft geloescht. Verknuepfungen, Exemplare und Beitraege werden entfernt.
|
||||
</p>
|
||||
<div class="flex items-center justify-end gap-3 mt-4">
|
||||
<button type="button" class="resetbutton w-auto px-3 py-1 text-sm" data-role="content-delete-cancel">Abbrechen</button>
|
||||
<button type="button" class="submitbutton w-auto bg-red-700 hover:bg-red-800 px-3 py-1 text-sm" data-role="content-delete-confirm">
|
||||
Loeschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
</form>
|
||||
</div>
|
||||
</edit-page>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const applyMultiSelectInit = (el) => {
|
||||
if (!el) return;
|
||||
const optionsRaw = el.getAttribute("data-initial-options") || "[]";
|
||||
const valuesRaw = el.getAttribute("data-initial-values") || "[]";
|
||||
let options = [];
|
||||
let values = [];
|
||||
try {
|
||||
options = JSON.parse(optionsRaw);
|
||||
values = JSON.parse(valuesRaw);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (options.length && typeof el.setOptions === "function") {
|
||||
el.setOptions(options);
|
||||
}
|
||||
if (values.length) {
|
||||
el.value = values;
|
||||
if (typeof el.captureInitialSelection === "function") {
|
||||
el.captureInitialSelection();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const deleteButton = document.querySelector("[data-role='content-delete']");
|
||||
const deleteDialog = document.querySelector("[data-role='content-delete-dialog']");
|
||||
const deleteConfirm = document.querySelector("[data-role='content-delete-confirm']");
|
||||
const deleteCancel = document.querySelector("[data-role='content-delete-cancel']");
|
||||
const csrfToken = "{{ $model.csrf_token }}";
|
||||
const contentId = "{{ $model.content_id }}";
|
||||
const deleteEndpoint = "/almanach/{{ $model.result.Entry.MusenalmID }}/contents/delete";
|
||||
|
||||
const initMultiSelects = () => {
|
||||
document.querySelectorAll("multi-select-simple[data-initial-options], multi-select-simple[data-initial-values]").forEach((el) => {
|
||||
applyMultiSelectInit(el);
|
||||
});
|
||||
};
|
||||
if (window.customElements?.whenDefined) {
|
||||
window.customElements.whenDefined("multi-select-simple").then(() => {
|
||||
requestAnimationFrame(initMultiSelects);
|
||||
});
|
||||
} else {
|
||||
initMultiSelects();
|
||||
}
|
||||
|
||||
if (deleteButton && deleteDialog && deleteConfirm && deleteCancel) {
|
||||
deleteButton.addEventListener("click", () => {
|
||||
if (deleteDialog.showModal) {
|
||||
deleteDialog.showModal();
|
||||
} else {
|
||||
deleteDialog.setAttribute("open", "true");
|
||||
}
|
||||
});
|
||||
|
||||
deleteCancel.addEventListener("click", () => {
|
||||
deleteDialog.close();
|
||||
});
|
||||
|
||||
deleteDialog.addEventListener("cancel", (event) => {
|
||||
event.preventDefault();
|
||||
deleteDialog.close();
|
||||
});
|
||||
|
||||
deleteConfirm.addEventListener("click", () => {
|
||||
const payload = new URLSearchParams();
|
||||
payload.set("csrf_token", csrfToken);
|
||||
payload.set("content_id", contentId);
|
||||
fetch(deleteEndpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: payload.toString(),
|
||||
}).then((response) => {
|
||||
if (response.ok) {
|
||||
window.location.assign("/almanach/{{ $model.result.Entry.MusenalmID }}/contents/edit");
|
||||
return;
|
||||
}
|
||||
deleteDialog.close();
|
||||
}).catch(() => {
|
||||
deleteDialog.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const showUserMessage = (message, type) => {
|
||||
const userMessage = document.getElementById("user-message");
|
||||
if (!userMessage) return;
|
||||
userMessage.textContent = message;
|
||||
userMessage.classList.remove("hidden", "save-feedback-error", "save-feedback-success", "text-red-700", "text-green-700");
|
||||
if (type === "error") {
|
||||
userMessage.classList.add("save-feedback-error", "text-red-700");
|
||||
} else if (type === "success") {
|
||||
userMessage.classList.add("save-feedback-success", "text-green-700");
|
||||
}
|
||||
};
|
||||
|
||||
const attachFormHandlers = () => {
|
||||
const form = document.querySelector("form.dbform");
|
||||
const uploadInput = document.querySelector("[data-role='content-images-upload-input']");
|
||||
const userMessage = document.getElementById("user-message");
|
||||
if (!form || !uploadInput || !userMessage) {
|
||||
return;
|
||||
}
|
||||
form.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event.stopImmediatePropagation();
|
||||
const submitter = event.submitter || document.activeElement;
|
||||
const files = Array.from(uploadInput.files || []);
|
||||
if (files.length > 0) {
|
||||
const hasInvalid = files.some((file) => !file.type || !file.type.startsWith("image/"));
|
||||
if (hasInvalid) {
|
||||
showUserMessage("Bitte nur Bilddateien auswählen.", "error");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const payload = new FormData(form);
|
||||
if (submitter && submitter.name) {
|
||||
payload.set(submitter.name, submitter.value || "");
|
||||
}
|
||||
if (payload.has("scans")) {
|
||||
payload.delete("scans");
|
||||
}
|
||||
const imagesComponent = document.querySelector("content-images");
|
||||
if (imagesComponent && typeof imagesComponent.getPendingFiles === "function") {
|
||||
const pendingFiles = imagesComponent.getPendingFiles();
|
||||
const pendingIds = typeof imagesComponent.getPendingIds === "function" ? imagesComponent.getPendingIds() : [];
|
||||
const scanOrder = typeof imagesComponent.getScanOrder === "function" ? imagesComponent.getScanOrder() : [];
|
||||
pendingFiles.forEach((file, index) => {
|
||||
payload.append("scans", file);
|
||||
if (pendingIds[index]) {
|
||||
payload.append("scans_pending_ids[]", pendingIds[index]);
|
||||
}
|
||||
});
|
||||
if (Array.isArray(scanOrder) && scanOrder.length > 0) {
|
||||
scanOrder.forEach((token) => {
|
||||
payload.append("scans_order[]", token);
|
||||
});
|
||||
}
|
||||
if (typeof imagesComponent.getPendingDeletes === "function") {
|
||||
imagesComponent.getPendingDeletes().forEach((fileName) => {
|
||||
payload.append("scans_delete[]", fileName);
|
||||
});
|
||||
}
|
||||
}
|
||||
const response = await fetch(form.action, {
|
||||
method: form.method || "POST",
|
||||
body: payload,
|
||||
credentials: "same-origin",
|
||||
});
|
||||
const html = await response.text();
|
||||
if (response.redirected && response.url) {
|
||||
if (response.url !== window.location.href) {
|
||||
window.location.assign(response.url);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!html) {
|
||||
return;
|
||||
}
|
||||
const doc = new DOMParser().parseFromString(html, "text/html");
|
||||
const nextMessage = doc.getElementById("user-message");
|
||||
const liveMessage = document.getElementById("user-message");
|
||||
const isErrorMessage = nextMessage && nextMessage.classList.contains("save-feedback-error");
|
||||
if (nextMessage && liveMessage) {
|
||||
liveMessage.className = nextMessage.className;
|
||||
liveMessage.textContent = nextMessage.textContent || "";
|
||||
if (!liveMessage.textContent.trim()) {
|
||||
liveMessage.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
if (isErrorMessage || !response.ok) {
|
||||
if (!nextMessage && !response.ok) {
|
||||
showUserMessage("Speichern fehlgeschlagen.", "error");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const nextForm = doc.querySelector("form.dbform");
|
||||
if (nextForm) {
|
||||
form.replaceWith(nextForm);
|
||||
initMultiSelects();
|
||||
attachFormHandlers();
|
||||
return;
|
||||
}
|
||||
}, true);
|
||||
};
|
||||
|
||||
attachFormHandlers();
|
||||
})();
|
||||
</script>
|
||||
@@ -1,10 +0,0 @@
|
||||
{{ $model := . }}
|
||||
<title>
|
||||
{{- if $model.content -}}
|
||||
Beitrag bearbeiten: {{ if $model.content.PreferredTitle }}{{ $model.content.PreferredTitle }}{{ else if $model.content.TitleStmt }}{{ $model.content.TitleStmt }}{{ else }}Beitrag{{ end }} - Musenalm
|
||||
{{- else if $model.result -}}
|
||||
Beitrag bearbeiten: {{ $model.result.Entry.PreferredTitle }} - Musenalm
|
||||
{{- else -}}
|
||||
Beitrag bearbeiten - Musenalm
|
||||
{{- end -}}
|
||||
</title>
|
||||
@@ -1,11 +0,0 @@
|
||||
{{- $content := index . "content" -}}
|
||||
{{- $entry := index . "entry" -}}
|
||||
{{- $csrf := index . "csrf_token" -}}
|
||||
{{- $isNew := index . "is_new" -}}
|
||||
|
||||
{{- template "_content_images_panel" (Dict
|
||||
"content" $content
|
||||
"entry" $entry
|
||||
"csrf_token" $csrf
|
||||
"is_new" $isNew
|
||||
) -}}
|
||||
@@ -1 +0,0 @@
|
||||
{{ template "_contents_list" . }}
|
||||
@@ -45,7 +45,7 @@ type AlmanachResult struct {
|
||||
<i class="ri-book-line"></i> Almanach
|
||||
</span>
|
||||
{{- if not $model.is_new -}}
|
||||
<a href="/almanach/{{- $model.result.Entry.MusenalmID -}}/contents/edit/" class="header-tab">
|
||||
<a href="/almanach/{{- $model.result.Entry.MusenalmID -}}/contents/edit" class="header-tab">
|
||||
<i class="ri-file-list-3-line"></i> Beiträge
|
||||
</a>
|
||||
{{- else -}}
|
||||
|
||||
Reference in New Issue
Block a user