Almanach list

This commit is contained in:
Simon Martens
2026-01-09 14:00:15 +01:00
parent 2d7751b4cb
commit ecfd3579a5
9 changed files with 797 additions and 10 deletions

View File

@@ -491,13 +491,17 @@ class Ge extends HTMLElement {
static get observedAttributes() {
}
constructor() {
super(), this._showall = !1, this.shown = -1, this._headings = [], this._contents = [], this._checkbox = null;
super(), this._showall = !1, this.shown = -1, this._headings = [], this._contents = [], this._checkbox = null, this._disabled = /* @__PURE__ */ new Set(), this._defaultIndex = null;
}
connectedCallback() {
this._headings = Array.from(this.querySelectorAll(".tab-list-head")), this._contents = Array.from(this.querySelectorAll(".tab-list-panel")), this.hookupEvtHandlers(), this.hideDependent(), this._headings.length === 1 && this.expand(0);
if (this._headings = Array.from(this.querySelectorAll(".tab-list-head")), this._contents = Array.from(this.querySelectorAll(".tab-list-panel")), this._readConfig(), this.hookupEvtHandlers(), this._applyDisabled(), this.hideDependent(), this._headings.length === 1) {
this.expand(0);
return;
}
this._defaultIndex !== null && this._expandFirstAvailable(this._defaultIndex);
}
expand(t) {
t < 0 || t >= this._headings.length || (this.shown = t, this._contents.forEach((e, i) => {
t < 0 || t >= this._headings.length || this._disabled.has(t) || (this.shown = t, this._contents.forEach((e, i) => {
i === t ? (e.classList.remove("hidden"), this._headings[i].setAttribute("aria-pressed", "true")) : (e.classList.add("hidden"), this._headings[i].setAttribute("aria-pressed", "false"));
}));
}
@@ -512,6 +516,32 @@ class Ge extends HTMLElement {
for (let t of this._contents)
t.classList.add("hidden");
}
_readConfig() {
const t = (this.getAttribute("data-disabled-indices") || "").trim(), e = (this.getAttribute("data-default-index") || "").trim();
if (this._disabled.clear(), t && t.split(",").map((i) => parseInt(i.trim(), 10)).filter((i) => Number.isFinite(i)).forEach((i) => this._disabled.add(i)), e !== "") {
const i = parseInt(e, 10);
this._defaultIndex = Number.isFinite(i) ? i : null;
} else
this._defaultIndex = null;
}
_applyDisabled() {
this._headings.forEach((t, e) => {
this._disabled.has(e) ? t.classList.add("pointer-events-none", "opacity-60") : t.classList.remove("pointer-events-none", "opacity-60");
});
}
_expandFirstAvailable(t) {
if (this._headings.length !== 0) {
if (!this._disabled.has(t)) {
this.expand(t);
return;
}
for (let e = 0; e < this._headings.length; e += 1)
if (!this._disabled.has(e)) {
this.expand(e);
return;
}
}
}
restore() {
for (let t of this._headings)
t.classList.add("cursor-pointer"), t.classList.add("select-none"), t.setAttribute("role", "button"), t.setAttribute("aria-pressed", "false"), t.setAttribute("tabindex", "0"), t.classList.remove("pointer-events-none"), t.classList.remove("!text-slate-900");
@@ -2966,9 +2996,44 @@ class li extends HTMLElement {
connectedCallback() {
setTimeout(() => {
const t = this.querySelector("form");
t && typeof window.FormLoad == "function" && window.FormLoad(t);
t && typeof window.FormLoad == "function" && window.FormLoad(t), this._setupDelete();
}, 0);
}
_setupDelete() {
const t = this.querySelector("form");
if (!t)
return;
const e = t.getAttribute("data-delete-endpoint");
if (!e)
return;
const i = this.querySelector("[data-role='edit-delete-dialog']"), s = this.querySelector("[data-role='edit-delete']"), n = this.querySelector("[data-role='edit-delete-confirm']"), a = this.querySelector("[data-role='edit-delete-cancel']");
if (!i || !s || !n || !a)
return;
s.addEventListener("click", (o) => {
o.preventDefault(), typeof i.showModal == "function" && i.showModal();
});
const r = (o) => {
o && o.preventDefault(), i.open && i.close();
};
a.addEventListener("click", r), i.addEventListener("cancel", r), n.addEventListener("click", async (o) => {
o.preventDefault(), r();
const d = new FormData(t), c = {
csrf_token: d.get("csrf_token") || "",
last_edited: d.get("last_edited") || ""
}, h = await fetch(e, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json"
},
body: JSON.stringify(c)
});
if (!h.ok)
return;
const u = await h.json().catch(() => null), m = (u == null ? void 0 : u.redirect) || "/";
window.location.assign(m);
});
}
}
const ri = "filter-list", oi = "scroll-button", di = "tool-tip", hi = "abbrev-tooltips", ci = "int-link", ui = "popup-image", mi = "tab-list", _i = "filter-pill", pi = "image-reel", fi = "multi-select-places", gi = "multi-select-simple", bi = "single-select-remote", Me = "reset-button", Ei = "div-manager", Si = "items-editor", vi = "almanach-edit-page", Li = "relations-editor", yi = "edit-page";
customElements.define(ci, je);

File diff suppressed because one or more lines are too long

View File

@@ -31,6 +31,35 @@
<div class="">{{ $agent.Id }}</div>
</div>
</div>
<div class="flex flex-col justify-end gap-y-6 pr-6">
<div class="">
<div class="font-bold text-sm">
<i class="ri-navigation-line"></i> Navigation
</div>
<div class="flex items-center gap-3">
{{- if $model.result.Prev -}}
<tool-tip position="top" class="!inline">
<div class="data-tip">{{ $model.result.Prev.Name }}</div>
<a
href="/person/{{ $model.result.Prev.Id }}/edit"
class="text-gray-700 hover:text-slate-950 no-underline">
<i class="ri-arrow-left-s-line"></i>
</a>
</tool-tip>
{{- end -}}
{{- if $model.result.Next -}}
<tool-tip position="top" class="!inline">
<div class="data-tip">{{ $model.result.Next.Name }}</div>
<a
href="/person/{{ $model.result.Next.Id }}/edit"
class="text-gray-700 hover:text-slate-950 no-underline">
<i class="ri-arrow-right-s-line"></i>
</a>
</tool-tip>
{{- end -}}
</div>
</div>
</div>
<div class="flex flex-col justify-end gap-y-6 pr-4">
<div class="">
<div class="font-bold text-sm mb-1"><i class="ri-calendar-line"></i> Zuletzt bearbeitet</div>
@@ -122,6 +151,85 @@
<label for="edit_comment" class="inputlabel">Bearbeitungsvermerk</label>
<textarea name="edit_comment" id="edit_comment" class="inputinput" autocomplete="off" rows="1">{{- $agent.Comment -}}</textarea>
</div>
<div class="mt-2">
<tab-list
data-default-index="{{ if gt (len $model.result.Entries) 0 }}0{{ else if gt (len $model.result.Contents) 0 }}1{{ end }}"
data-disabled-indices="{{ if and (eq (len $model.result.Entries) 0) (eq (len $model.result.Contents) 0) }}0,1{{ else if eq (len $model.result.Entries) 0 }}0{{ else if eq (len $model.result.Contents) 0 }}1{{ end }}">
<div class="flex items-center gap-3 text-sm font-bold text-gray-700">
<div class="tab-list-head flex items-center gap-2">
<i class="ri-book-2-line"></i>
<span>Verknüpfte Bände</span>
<span class="text-xs bg-stone-200 text-gray-700 px-2 py-0.5 rounded-sm">{{ len $model.result.Entries }}</span>
</div>
<div class="tab-list-head flex items-center gap-2">
<i class="ri-article-line"></i>
<span>Verknüpfte Inhalte</span>
<span class="text-xs bg-stone-200 text-gray-700 px-2 py-0.5 rounded-sm">{{ len $model.result.Contents }}</span>
</div>
</div>
<hr class="border-slate-400 mt-2 mb-3" />
<div class="tab-list-panel text-sm text-gray-700 max-h-96 overflow-auto pr-1 pl-0 ml-0">
{{- if $model.result.Entries -}}
<ul class="flex flex-col gap-3 pl-0 pr-0 m-0 ml-0 list-none">
{{- range $entry := $model.result.Entries -}}
{{- $entryTypes := index $model.result.EntryTypes $entry.Id -}}
<li class="flex items-baseline justify-between gap-3 ml-0 pl-0">
<div class="flex flex-col gap-1">
<a href="/almanach/{{ $entry.MusenalmID }}" class="no-underline hover:text-slate-900">
{{- $entry.PreferredTitle -}}
</a>
{{- if $entryTypes -}}
<div class="text-xs text-gray-600">
Rolle:
{{- range $i, $t := $entryTypes -}}
{{- if $i }}, {{ end -}}{{ $t -}}
{{- end -}}
</div>
{{- end -}}
</div>
<span class="text-xs text-gray-500">{{ $entry.Year }}</span>
</li>
{{- end -}}
</ul>
{{- else -}}
<div class="italic text-gray-500">Keine Bände verknüpft.</div>
{{- end -}}
</div>
<div class="tab-list-panel text-sm text-gray-700 max-h-96 overflow-auto pr-1 pl-0 ml-0">
{{- if $model.result.Contents -}}
<ul class="flex flex-col gap-3 pl-0 pr-0 m-0 ml-0 list-none">
{{- range $content := $model.result.Contents -}}
{{- $entry := index $model.result.ContentEntries $content.Entry -}}
{{- $types := index $model.result.ContentTypes $content.Id -}}
<li class="flex flex-col gap-1 ml-0 pl-0">
<a href="/beitrag/{{ $content.MusenalmID }}" class="no-underline hover:text-slate-900 font-semibold">
{{- if $content.PreferredTitle -}}{{ $content.PreferredTitle }}{{- else -}}Inhalt #{{ $content.MusenalmID }}{{- end -}}
</a>
<div class="text-xs text-gray-600 flex flex-wrap gap-3">
{{- if $entry -}}
<span>Band: <a href="/almanach/{{ $entry.MusenalmID }}" class="no-underline hover:text-slate-900">{{ $entry.PreferredTitle }}</a></span>
{{- end -}}
{{- if $types -}}
<span>
Rolle:
{{- range $i, $t := $types -}}
{{- if $i }}, {{ end -}}{{ $t -}}
{{- end -}}
</span>
{{- end -}}
{{- if $content.MusenalmPagination -}}
<span>Seite: {{ $content.MusenalmPagination }}</span>
{{- end -}}
</div>
</li>
{{- end -}}
</ul>
{{- else -}}
<div class="italic text-gray-500">Keine Inhalte verknüpft.</div>
{{- end -}}
</div>
</tab-list>
</div>
</div>
</div>

View File

@@ -31,6 +31,35 @@
<div class="">{{ $series.Id }}</div>
</div>
</div>
<div class="flex flex-col justify-end gap-y-6 pr-6">
<div class="">
<div class="font-bold text-sm">
<i class="ri-navigation-line"></i> Navigation
</div>
<div class="flex items-center gap-3">
{{- if $model.result.Prev -}}
<tool-tip position="top" class="!inline">
<div class="data-tip">{{ $model.result.Prev.Title }}</div>
<a
href="/reihe/{{ $model.result.Prev.MusenalmID }}/edit"
class="text-gray-700 hover:text-slate-950 no-underline">
<i class="ri-arrow-left-s-line"></i>
</a>
</tool-tip>
{{- end -}}
{{- if $model.result.Next -}}
<tool-tip position="top" class="!inline">
<div class="data-tip">{{ $model.result.Next.Title }}</div>
<a
href="/reihe/{{ $model.result.Next.MusenalmID }}/edit"
class="text-gray-700 hover:text-slate-950 no-underline">
<i class="ri-arrow-right-s-line"></i>
</a>
</tool-tip>
{{- end -}}
</div>
</div>
</div>
<div class="flex flex-col justify-end gap-y-6 pr-4">
<div class="">
<div class="font-bold text-sm mb-1"><i class="ri-calendar-line"></i> Zuletzt bearbeitet</div>
@@ -58,7 +87,8 @@
class="w-full dbform"
id="changeseriesform"
method="POST"
action="/reihe/{{ $series.MusenalmID }}/edit">
action="/reihe/{{ $series.MusenalmID }}/edit"
data-delete-endpoint="/reihe/{{ $series.MusenalmID }}/edit/delete">
<input type="hidden" name="csrf_token" value="{{ $model.csrf_token }}" />
<input type="hidden" name="last_edited" value="{{ $series.Updated }}" />
@@ -101,6 +131,74 @@
<label for="edit_comment" class="inputlabel">Bearbeitungsvermerk</label>
<textarea name="edit_comment" id="edit_comment" class="inputinput" autocomplete="off" rows="1">{{- $series.Comment -}}</textarea>
</div>
<div class="mt-2">
<tab-list
data-default-index="{{ if gt (len $model.result.Entries) 0 }}0{{ else if gt (len $model.result.Contents) 0 }}1{{ end }}"
data-disabled-indices="{{ if and (eq (len $model.result.Entries) 0) (eq (len $model.result.Contents) 0) }}0,1{{ else if eq (len $model.result.Entries) 0 }}0{{ else if eq (len $model.result.Contents) 0 }}1{{ end }}">
<div class="flex items-center gap-3 text-sm font-bold text-gray-700">
<div class="tab-list-head flex items-center gap-2">
<i class="ri-book-2-line"></i>
<span>Verknüpfte Bände</span>
<span class="text-xs bg-stone-200 text-gray-700 px-2 py-0.5 rounded-sm">{{ len $model.result.Entries }}</span>
</div>
<div class="tab-list-head flex items-center gap-2">
<i class="ri-article-line"></i>
<span>Verknüpfte Inhalte</span>
<span class="text-xs bg-stone-200 text-gray-700 px-2 py-0.5 rounded-sm">{{ len $model.result.Contents }}</span>
</div>
</div>
<hr class="border-slate-400 mt-2 mb-3" />
<div class="tab-list-panel text-sm text-gray-700 max-h-96 overflow-auto pr-1 pl-0 ml-0">
{{- if $model.result.Entries -}}
<ul class="flex flex-col gap-2 pl-0 pr-0 m-0 ml-0 list-none">
{{- range $entry := $model.result.Entries -}}
<li class="flex items-baseline justify-between gap-3 ml-0 pl-0">
<a href="/almanach/{{ $entry.MusenalmID }}" class="no-underline hover:text-slate-900">
{{- $entry.PreferredTitle -}}
</a>
<span class="text-xs text-gray-500">{{ $entry.Year }}</span>
</li>
{{- end -}}
</ul>
{{- else -}}
<div class="italic text-gray-500">Keine Bände verknüpft.</div>
{{- end -}}
</div>
<div class="tab-list-panel text-sm text-gray-700 max-h-96 overflow-auto pr-1 pl-0 ml-0">
{{- if $model.result.Contents -}}
<ul class="flex flex-col gap-3 pl-0 pr-0 m-0 ml-0 list-none">
{{- range $content := $model.result.Contents -}}
{{- $entry := index $model.result.ContentEntries $content.Entry -}}
{{- $types := index $model.result.ContentTypes $content.Id -}}
<li class="flex flex-col gap-1 ml-0 pl-0">
<a href="/beitrag/{{ $content.MusenalmID }}" class="no-underline hover:text-slate-900 font-semibold">
{{- if $content.PreferredTitle -}}{{ $content.PreferredTitle }}{{- else -}}Inhalt #{{ $content.MusenalmID }}{{- end -}}
</a>
<div class="text-xs text-gray-600 flex flex-wrap gap-3">
{{- if $entry -}}
<span>Band: <a href="/almanach/{{ $entry.MusenalmID }}" class="no-underline hover:text-slate-900">{{ $entry.PreferredTitle }}</a></span>
{{- end -}}
{{- if $types -}}
<span>
Typ:
{{- range $i, $t := $types -}}
{{- if $i }}, {{ end -}}{{ $t -}}
{{- end -}}
</span>
{{- end -}}
{{- if $content.MusenalmPagination -}}
<span>Seite: {{ $content.MusenalmPagination }}</span>
{{- end -}}
</div>
</li>
{{- end -}}
</ul>
{{- else -}}
<div class="italic text-gray-500">Keine Inhalte verknüpft.</div>
{{- end -}}
</div>
</tab-list>
</div>
</div>
</div>
@@ -115,6 +213,13 @@
<i class="ri-loop-left-line"></i>
<span>Reset</span>
</a>
<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="edit-delete">
<i class="ri-delete-bin-line"></i>
<span>Reihe löschen</span>
</button>
<button type="submit" class="submitbutton w-40 flex items-center gap-2 justify-center">
<i class="ri-save-line"></i>
<span>Speichern</span>
@@ -123,4 +228,36 @@
</div>
</form>
</div>
<dialog data-role="edit-delete-dialog" class="fixed inset-0 m-auto rounded-md border border-slate-200 p-0 shadow-xl backdrop:bg-black/40">
<div class="p-5 w-[26rem]">
<div class="text-base font-bold text-gray-900">Reihe löschen?</div>
<div class="text-sm font-bold text-gray-900 mt-1">{{ $series.Title }}</div>
<p class="text-sm text-gray-700 mt-2">
Alle Bände, Inhalte und Verknüpfungen der bevorzugten Reihentitel werden gelöscht.
</p>
<div class="mt-3">
<div class="text-sm font-semibold text-gray-700">Betroffene Bände</div>
<div class="mt-2 max-h-40 overflow-auto pr-1">
{{- if $model.result.PreferredEntries -}}
<ul class="flex flex-col gap-2 pl-0 pr-0 m-0 list-none">
{{- range $entry := $model.result.PreferredEntries -}}
<li class="flex items-baseline justify-between gap-3 ml-0 pl-0 text-sm text-gray-700">
<span>{{ $entry.PreferredTitle }}</span>
<span class="text-xs text-gray-500">{{ $entry.Year }}</span>
</li>
{{- end -}}
</ul>
{{- else -}}
<div class="italic text-gray-500">Keine Bände betroffen.</div>
{{- end -}}
</div>
</div>
<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="edit-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="edit-delete-confirm">
Löschen
</button>
</div>
</div>
</dialog>
</edit-page>

View File

@@ -5,6 +5,69 @@ export class EditPage extends HTMLElement {
if (form && typeof window.FormLoad === "function") {
window.FormLoad(form);
}
this._setupDelete();
}, 0);
}
_setupDelete() {
const form = this.querySelector("form");
if (!form) {
return;
}
const deleteEndpoint = form.getAttribute("data-delete-endpoint");
if (!deleteEndpoint) {
return;
}
const dialog = this.querySelector("[data-role='edit-delete-dialog']");
const deleteButton = this.querySelector("[data-role='edit-delete']");
const confirmButton = this.querySelector("[data-role='edit-delete-confirm']");
const cancelButton = this.querySelector("[data-role='edit-delete-cancel']");
if (!dialog || !deleteButton || !confirmButton || !cancelButton) {
return;
}
deleteButton.addEventListener("click", (event) => {
event.preventDefault();
if (typeof dialog.showModal === "function") {
dialog.showModal();
}
});
const closeDialog = (event) => {
if (event) {
event.preventDefault();
}
if (dialog.open) {
dialog.close();
}
};
cancelButton.addEventListener("click", closeDialog);
dialog.addEventListener("cancel", closeDialog);
confirmButton.addEventListener("click", async (event) => {
event.preventDefault();
closeDialog();
const formData = new FormData(form);
const payload = {
csrf_token: formData.get("csrf_token") || "",
last_edited: formData.get("last_edited") || "",
};
const response = await fetch(deleteEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
return;
}
const data = await response.json().catch(() => null);
const redirect = data?.redirect || "/";
window.location.assign(redirect);
});
}
}

View File

@@ -102,6 +102,18 @@
@apply px-1.5 italic text-gray-600;
}
/* Reset global list indentation inside tab panels */
.tab-list-panel ul {
margin-left: 0;
padding-left: 0;
list-style: none;
}
.tab-list-panel li {
margin-left: 0;
padding-left: 0;
}
/* Disabled form controls in deleted relations */
[data-rel-row].bg-red-50 select:disabled,
[data-rel-row].bg-red-50 input[type="checkbox"]:disabled:not([data-delete-toggle]) {

View File

@@ -8,16 +8,25 @@ export class TabList extends HTMLElement {
this._headings = [];
this._contents = [];
this._checkbox = null;
this._disabled = new Set();
this._defaultIndex = null;
}
connectedCallback() {
this._headings = Array.from(this.querySelectorAll(".tab-list-head"));
this._contents = Array.from(this.querySelectorAll(".tab-list-panel"));
this._readConfig();
this.hookupEvtHandlers();
this._applyDisabled();
this.hideDependent();
if (this._headings.length === 1) {
this.expand(0);
return;
}
if (this._defaultIndex !== null) {
this._expandFirstAvailable(this._defaultIndex);
}
}
@@ -25,6 +34,9 @@ export class TabList extends HTMLElement {
if (index < 0 || index >= this._headings.length) {
return;
}
if (this._disabled.has(index)) {
return;
}
this.shown = index;
@@ -67,6 +79,53 @@ export class TabList extends HTMLElement {
}
}
_readConfig() {
const disabledRaw = (this.getAttribute("data-disabled-indices") || "").trim();
const defaultRaw = (this.getAttribute("data-default-index") || "").trim();
this._disabled.clear();
if (disabledRaw) {
disabledRaw
.split(",")
.map((value) => parseInt(value.trim(), 10))
.filter((value) => Number.isFinite(value))
.forEach((value) => this._disabled.add(value));
}
if (defaultRaw !== "") {
const parsed = parseInt(defaultRaw, 10);
this._defaultIndex = Number.isFinite(parsed) ? parsed : null;
} else {
this._defaultIndex = null;
}
}
_applyDisabled() {
this._headings.forEach((heading, index) => {
if (this._disabled.has(index)) {
heading.classList.add("pointer-events-none", "opacity-60");
} else {
heading.classList.remove("pointer-events-none", "opacity-60");
}
});
}
_expandFirstAvailable(preferredIndex) {
if (this._headings.length === 0) {
return;
}
if (!this._disabled.has(preferredIndex)) {
this.expand(preferredIndex);
return;
}
for (let i = 0; i < this._headings.length; i += 1) {
if (!this._disabled.has(i)) {
this.expand(i);
return;
}
}
}
restore() {
for (let heading of this._headings) {
heading.classList.add("cursor-pointer");