+BUGFIX: do not load in the whole edit form in inhalte edit. Fetch on demand

This commit is contained in:
Simon Martens
2026-01-16 20:05:04 +01:00
parent d4bdaea2b7
commit 6583114773
5 changed files with 256 additions and 94 deletions

View File

@@ -134,7 +134,11 @@
<span data-role="contents-collapse-all-label">Alle Eintraege einklappen</span>
</button>
</div>
<div class="flex flex-col gap-1" data-role="contents-list" data-insert-endpoint="/almanach/{{ $model.result.Entry.MusenalmID }}/contents/insert">
<div class="flex flex-col gap-1"
data-role="contents-list"
data-insert-endpoint="/almanach/{{ $model.result.Entry.MusenalmID }}/contents/insert"
data-edit-endpoint="/almanach/{{ $model.result.Entry.MusenalmID }}/contents/edit/form"
data-order-endpoint="/almanach/{{ $model.result.Entry.MusenalmID }}/contents/edit">
{{- range $_, $content := $model.result.Contents -}}
{{- template "_content_item" (Dict
"content" $content
@@ -178,6 +182,9 @@
}
};
let enterEditMode = null;
let setupEditFormGlobal = null;
const initPage = () => {
const list = document.querySelector("[data-role='contents-list']");
if (!list) {
@@ -227,7 +234,7 @@
if (item.parentElement !== list) {
return;
}
const contentId = item.querySelector("[data-role='content-card']")?.dataset.contentId || "";
const contentId = item.dataset.contentId || "";
list.insertBefore(createGap("before", contentId, false), item);
});
list.appendChild(createGap("after", "", true));
@@ -261,7 +268,7 @@
}
const isTempContentId = (contentId) => contentId && contentId.startsWith("tmp");
const orderEndpoint = getItems()[0]?.querySelector("form")?.getAttribute("action") || "";
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");
@@ -287,7 +294,7 @@
const payload = new URLSearchParams();
payload.set("csrf_token", csrfToken);
list.querySelectorAll("[data-role='content-item']").forEach((card) => {
const contentId = card.querySelector("[data-role='content-card']")?.dataset.contentId;
const contentId = card.dataset.contentId;
if (!contentId || isTempContentId(contentId)) {
return;
}
@@ -319,8 +326,11 @@
}, 300);
};
const closeAll = () => {
const closeAll = (keepItem = null) => {
getItems().forEach((item) => {
if (keepItem && item === keepItem) {
return;
}
const view = item.querySelector("[data-role='content-view']");
const edit = item.querySelector("[data-role='content-edit']");
const editButton = item.querySelector("[data-role='content-edit-button']");
@@ -329,7 +339,7 @@
view.classList.remove("hidden");
}
if (edit) {
edit.classList.add("hidden");
edit.remove();
}
if (editButton) {
editButton.classList.remove("hidden");
@@ -337,15 +347,12 @@
});
};
const openItem = (item) => {
closeAll();
enterEditMode = (item) => {
const view = item.querySelector("[data-role='content-view']");
const edit = item.querySelector("[data-role='content-edit']");
if (view && edit) {
if (view) {
view.classList.add("hidden");
edit.classList.remove("hidden");
item.classList.add("data-editing");
}
item.classList.add("data-editing");
setEditSpacing(true);
removeGaps();
getItems().forEach((other) => {
@@ -357,6 +364,20 @@
});
};
const openItem = (item) => {
closeAll(item);
const edit = item.querySelector("[data-role='content-edit']");
if (!edit) {
item.dataset.pendingEdit = "true";
const editButton = item.querySelector("[data-role='content-edit-button']");
if (editButton) {
editButton.click();
}
return;
}
enterEditMode(item);
};
const removeItem = (item) => {
const gap = item.previousElementSibling;
if (gap && gap.matches("[data-role='content-gap']")) {
@@ -379,18 +400,11 @@
item.dataset.init = "true";
const editButton = item.querySelector("[data-role='content-edit-button']");
const cancelButton = item.querySelector("[data-role='content-edit-cancel']");
const deleteButton = item.querySelector("[data-role='content-delete']");
const deleteDialog = item.querySelector("[data-role='content-delete-dialog']");
const deleteConfirm = item.querySelector("[data-role='content-delete-confirm']");
const deleteCancel = item.querySelector("[data-role='content-delete-cancel']");
const view = item.querySelector("[data-role='content-view']");
const edit = item.querySelector("[data-role='content-edit']");
const collapseButton = item.querySelector("[data-role='content-collapse-toggle']");
const collapseIcon = item.querySelector("[data-role='content-collapse-icon']");
const collapsedSummary = item.querySelector("[data-role='content-collapsed-summary']");
const viewBody = item.querySelector("[data-role='content-view-body']");
const form = item.querySelector("form");
const setCollapsed = (collapsed) => {
if (!viewBody || !collapsedSummary) {
@@ -421,27 +435,57 @@
});
}
if (editButton && view && edit) {
if (editButton) {
editButton.addEventListener("click", () => {
openItem(item);
if (item.querySelector("[data-role='content-edit']")) {
enterEditMode(item);
return;
}
item.dataset.pendingEdit = "true";
});
}
if (cancelButton && view && edit) {
const pendingEdit = item.dataset.pendingEdit === "true";
if (pendingEdit && item.querySelector("[data-role='content-edit']")) {
item.dataset.pendingEdit = "";
enterEditMode(item);
}
item.querySelectorAll("multi-select-simple[data-initial-options], multi-select-simple[data-initial-values]").forEach((el) => {
applyMultiSelectInit(el);
});
if (item.dataset.openEdit === "true") {
item.dataset.openEdit = "";
openItem(item);
}
};
const setupEditForm = (item) => {
const edit = item.querySelector("[data-role='content-edit']");
if (!edit || edit.dataset.init === "true") {
return;
}
edit.dataset.init = "true";
const cancelButton = edit.querySelector("[data-role='content-edit-cancel']");
const deleteButton = edit.querySelector("[data-role='content-delete']");
const deleteDialog = edit.querySelector("[data-role='content-delete-dialog']");
const deleteConfirm = edit.querySelector("[data-role='content-delete-confirm']");
const deleteCancel = edit.querySelector("[data-role='content-delete-cancel']");
const form = edit.querySelector("form");
const view = item.querySelector("[data-role='content-view']");
if (cancelButton && view) {
cancelButton.addEventListener("click", () => {
if (item.dataset.contentTemp === "true") {
removeItem(item);
return;
}
edit.classList.add("hidden");
edit.remove();
view.classList.remove("hidden");
item.classList.remove("data-editing");
getItems().forEach((other) => {
const otherButton = other.querySelector("[data-role='content-edit-button']");
if (otherButton) {
otherButton.classList.remove("hidden");
}
});
showEditButtonsIfIdle();
syncEditSpacing();
renderInsertGaps();
});
@@ -476,7 +520,7 @@
}
const payload = new URLSearchParams();
payload.set("csrf_token", csrfToken);
payload.set("content_id", item.querySelector("[data-role='content-card']")?.dataset.contentId || "");
payload.set("content_id", item.dataset.contentId || "");
fetch(deleteEndpoint, {
method: "POST",
headers: {
@@ -499,7 +543,7 @@
form.addEventListener("submit", () => {
form.querySelectorAll("input[name='content_order[]']").forEach((input) => input.remove());
getItems().forEach((card) => {
const contentId = card.querySelector("[data-role='content-card']")?.dataset.contentId;
const contentId = card.dataset.contentId;
if (!contentId) {
return;
}
@@ -511,16 +555,8 @@
});
});
}
item.querySelectorAll("multi-select-simple[data-initial-options], multi-select-simple[data-initial-values]").forEach((el) => {
applyMultiSelectInit(el);
});
if (item.dataset.openEdit === "true") {
item.dataset.openEdit = "";
openItem(item);
}
};
setupEditFormGlobal = setupEditForm;
const updateCollapseAllLabel = () => {
if (!collapseAllButton || !collapseAllLabel) {
@@ -575,7 +611,10 @@
}
}
getItems().forEach((item) => setupItem(item));
getItems().forEach((item) => {
setupItem(item);
setupEditForm(item);
});
renderInsertGaps();
syncEditSpacing();
showEditButtonsIfIdle();
@@ -679,7 +718,7 @@
const editContentId = params.get("edit_content");
if (editContentId) {
const targetItem = getItems().find((item) => {
return item.querySelector(`[data-role='content-card'][data-content-id='${editContentId}']`);
return item.dataset.contentId === editContentId;
});
if (targetItem) {
openItem(targetItem);
@@ -700,7 +739,18 @@
initWhenReady();
document.body.addEventListener("htmx:afterSwap", () => {
document.body.addEventListener("htmx:afterSwap", (event) => {
const target = event.detail?.target;
if (target && target.matches("[data-role='content-edit-container']")) {
const item = target.closest("[data-role='content-item']");
if (item && item.dataset.pendingEdit === "true" && enterEditMode) {
item.dataset.pendingEdit = "";
enterEditMode(item);
if (setupEditFormGlobal) {
setupEditFormGlobal(item);
}
}
}
initWhenReady();
});
</script>

View File

@@ -0,0 +1,20 @@
{{- $content := index . "content" -}}
{{- $entry := index . "entry" -}}
{{- $csrf := index . "csrf_token" -}}
{{- $contentTypes := index . "content_types" -}}
{{- $musenalmTypes := index . "musenalm_types" -}}
{{- $paginationValues := index . "pagination_values" -}}
{{- $contentID := index . "content_id" -}}
{{- template "_content_edit_form" (Dict
"content" $content
"content_id" $contentID
"entry" $entry
"csrf_token" $csrf
"content_types" $contentTypes
"musenalm_types" $musenalmTypes
"pagination_values" $paginationValues
"agents" (index . "agents")
"content_agents" (index . "content_agents")
"agent_relations" (index . "agent_relations")
) -}}

View File

@@ -0,0 +1,72 @@
{{- $content := index . "content" -}}
{{- $entry := index . "entry" -}}
{{- $csrf := index . "csrf_token" -}}
{{- $contentTypes := index . "content_types" -}}
{{- $musenalmTypes := index . "musenalm_types" -}}
{{- $paginationValues := index . "pagination_values" -}}
{{- $agents := index . "agents" -}}
{{- $contentAgents := index . "content_agents" -}}
{{- $agentRelations := index . "agent_relations" -}}
{{- $contentID := index . "content_id" -}}
{{- $error := index . "error" -}}
<div data-role="content-edit" class="mt-2">
<form
autocomplete="off"
class="w-full dbform"
method="POST"
hx-boost="false"
hx-post="/almanach/{{ $entry.MusenalmID }}/contents/edit"
hx-target="closest [data-role='content-item']"
hx-swap="outerHTML"
action="/almanach/{{ $entry.MusenalmID }}/contents/edit">
<input type="hidden" name="csrf_token" value="{{ $csrf }}" />
{{- if $error -}}
<div class="mb-3 rounded-xs border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-800">
{{ $error }}
</div>
{{- end -}}
{{- template "_content_edit" (Dict
"content" $content
"content_id" $contentID
"entry" $entry
"content_types" $contentTypes
"musenalm_types" $musenalmTypes
"pagination_values" $paginationValues
"agents" $agents
"content_agents" $contentAgents
"agent_relations" $agentRelations
) -}}
<div class="w-full flex items-center justify-end gap-3 mt-4 flex-wrap">
<button type="button" class="resetbutton w-40 flex items-center gap-2 justify-center" data-role="content-edit-cancel">
<i class="ri-close-line"></i>
<span>Verwerfen</span>
</button>
<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 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>
</button>
</div>
</form>
<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 löschen?</div>
{{- if $content.TitleStmt -}}
<div class="text-sm font-bold text-gray-900 mt-1">{{ $content.TitleStmt }}</div>
{{- end -}}
<p class="text-sm text-gray-700 mt-2">
Der Eintrag wird dauerhaft gelöscht. Verknüpfungen, Exemplare und Inhalte 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>
</div>

View File

@@ -16,8 +16,9 @@
{{- if and $overrideID (ne $overrideID "") -}}
{{- $contentID = $overrideID -}}
{{- end -}}
{{- $editContainerID := printf "content-%s-edit-container" $contentID -}}
<div data-role="content-item" class="relative {{ if $isNew }}data-new-content{{ end }} {{ if $openEdit }}data-editing{{ end }}" data-open-edit="{{ if $openEdit }}true{{ end }}" data-content-temp="{{ if $isNew }}true{{ end }}">
<div data-role="content-item" class="relative {{ if $isNew }}data-new-content{{ end }} {{ if $openEdit }}data-editing{{ end }}" data-open-edit="{{ if $openEdit }}true{{ end }}" data-content-temp="{{ if $isNew }}true{{ end }}" data-content-id="{{ $contentID }}">
<div data-role="content-view" class="{{ if $openEdit }}hidden{{ end }} mt-2">
<div class="border border-slate-200 bg-stone-100 rounded-xs overflow-hidden">
<div class="flex items-center justify-between gap-4 border-b border-slate-200 bg-stone-200 px-3 py-2 cursor-grab" data-role="content-drag-handle" draggable="true" aria-label="Beitrag verschieben">
@@ -56,7 +57,14 @@
<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 eq $content.EditState "Edited" -}}Erfasst{{- else if eq $content.EditState "Review" -}}Überprüfen{{- else if eq $content.EditState "ToDo" -}}Zu erledigen{{- else if eq $content.EditState "Seen" -}}Autopsiert{{- else -}}Unbekannt{{- end -}}
</span>
<button type="button" class="resetbutton w-32 flex items-center gap-2 justify-center" data-role="content-edit-button">
<button
type="button"
class="resetbutton w-32 flex items-center gap-2 justify-center"
data-role="content-edit-button"
hx-boost="false"
hx-get="/almanach/{{ $entry.MusenalmID }}/contents/edit/form?content_id={{ $contentID }}"
hx-target="#{{ $editContainerID }}"
hx-swap="innerHTML">
<i class="ri-edit-2-line"></i>
<span>Bearbeiten</span>
</button>
@@ -109,64 +117,21 @@
</div>
</div>
</div>
<div data-role="content-edit" class="{{ if not $openEdit }}hidden{{ end }} mt-2">
<form
autocomplete="off"
class="w-full dbform"
method="POST"
hx-boost="false"
hx-post="/almanach/{{ $entry.MusenalmID }}/contents/edit"
hx-target="closest [data-role='content-item']"
hx-swap="outerHTML"
action="/almanach/{{ $entry.MusenalmID }}/contents/edit">
<input type="hidden" name="csrf_token" value="{{ $csrf }}" />
{{- if $error -}}
<div class="mb-3 rounded-xs border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-800">
{{ $error }}
</div>
{{- end -}}
{{- template "_content_edit" (Dict
<div data-role="content-edit-container" id="{{ $editContainerID }}">
{{- if $openEdit -}}
{{- template "_content_edit_form" (Dict
"content" $content
"content_id" $contentID
"entry" $entry
"csrf_token" $csrf
"content_types" $contentTypes
"musenalm_types" $musenalmTypes
"pagination_values" $paginationValues
"agents" $agents
"content_agents" $contentAgents
"agent_relations" $agentRelations
"error" $error
) -}}
<div class="w-full flex items-center justify-end gap-3 mt-4 flex-wrap">
<button type="button" class="resetbutton w-40 flex items-center gap-2 justify-center" data-role="content-edit-cancel">
<i class="ri-close-line"></i>
<span>Verwerfen</span>
</button>
<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 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>
</button>
</div>
</form>
<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 löschen?</div>
{{- if $content.TitleStmt -}}
<div class="text-sm font-bold text-gray-900 mt-1">{{ $content.TitleStmt }}</div>
{{- end -}}
<p class="text-sm text-gray-700 mt-2">
Der Eintrag wird dauerhaft gelöscht. Verknüpfungen, Exemplare und Inhalte 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>
{{- end -}}
</div>
</div>