+density on list

This commit is contained in:
Simon Martens
2026-01-16 23:04:07 +01:00
parent 0842f270b0
commit 0c63846024
8 changed files with 331 additions and 98 deletions

View File

@@ -25,6 +25,7 @@ const (
URL_ALMANACH_CONTENTS_INSERT = "contents/insert"
URL_ALMANACH_CONTENTS_DELETE = "contents/delete"
URL_ALMANACH_CONTENTS_EDIT_FORM = "contents/edit/form"
URL_ALMANACH_CONTENTS_EDIT_EXTENT = "contents/edit/extent"
TEMPLATE_ALMANACH_CONTENTS_EDIT = "/almanach/contents/edit/"
TEMPLATE_ALMANACH_CONTENTS_EDIT_FORM = "/almanach/contents/edit_form/"
)
@@ -54,6 +55,7 @@ func (p *AlmanachContentsEditPage) Setup(router *router.Router[*core.RequestEven
rg.GET(URL_ALMANACH_CONTENTS_EDIT_FORM, p.GETEditForm(engine, app))
rg.POST(URL_ALMANACH_CONTENTS_INSERT, p.POSTInsert(engine, app))
rg.POST(URL_ALMANACH_CONTENTS_DELETE, p.POSTDelete(engine, app))
rg.POST(URL_ALMANACH_CONTENTS_EDIT_EXTENT, p.POSTUpdateExtent(engine, app))
return nil
}
@@ -428,10 +430,51 @@ func (p *AlmanachContentsEditPage) POSTInsert(engine *templating.Engine, app cor
}
}
func (p *AlmanachContentsEditPage) POSTUpdateExtent(engine *templating.Engine, app core.App) HandleFunc {
return func(e *core.RequestEvent) error {
id := e.Request.PathValue("id")
req := templating.NewRequest(e)
if err := e.Request.ParseForm(); err != nil {
return p.renderError(engine, app, e, "Formulardaten ungültig.")
}
if err := req.CheckCSRF(e.Request.FormValue("csrf_token")); err != nil {
return p.renderError(engine, app, e, err.Error())
}
entry, err := dbmodels.Entries_MusenalmID(app, id)
if err != nil {
return engine.Response404(e, err, nil)
}
entry.SetExtent(strings.TrimSpace(e.Request.FormValue("extent")))
if user := req.User(); user != nil {
entry.SetEditor(user.Id)
}
if err := app.Save(entry); err != nil {
app.Logger().Error("Failed to update entry extent", "entry_id", entry.Id, "error", err)
return p.renderError(engine, app, e, "Struktur/Umfang konnte nicht gespeichert werden.")
}
InvalidateSortedEntriesCache()
go func(appInstance core.App, entryRecord *dbmodels.Entry) {
if err := updateEntryFTS5WithContents(appInstance, entryRecord, false); err != nil {
appInstance.Logger().Error("Failed to update entry FTS5", "entry_id", entryRecord.Id, "error", err)
}
}(app, entry)
redirect := fmt.Sprintf("/almanach/%s/contents/edit?saved_message=%s", id, url.QueryEscape("Struktur/Umfang gespeichert."))
return e.Redirect(http.StatusSeeOther, redirect)
}
}
func (p *AlmanachContentsEditPage) POSTDelete(engine *templating.Engine, app core.App) HandleFunc {
return func(e *core.RequestEvent) error {
id := e.Request.PathValue("id")
req := templating.NewRequest(e)
isHTMX := strings.EqualFold(e.Request.Header.Get("HX-Request"), "true")
if err := e.Request.ParseForm(); err != nil {
return p.renderError(engine, app, e, "Formulardaten ungültig.")
@@ -506,6 +549,11 @@ func (p *AlmanachContentsEditPage) POSTDelete(engine *templating.Engine, app cor
go updateContentsFTS5(app, entry, remaining)
}
if isHTMX {
success := `<div hx-swap-oob="innerHTML:#user-message"><div class="text-green-800 text-sm mt-2 rounded-xs bg-green-200 p-2 font-bold border-green-700 shadow border mb-3"><i class="ri-checkbox-circle-fill"></i> Beitrag geloescht.</div></div>`
return e.HTML(http.StatusOK, success)
}
redirect := fmt.Sprintf("/almanach/%s/contents/edit", id)
return e.Redirect(http.StatusSeeOther, redirect)
}

View File

@@ -5838,12 +5838,12 @@ class Nl extends HTMLElement {
return ["position", "timeout"];
}
constructor() {
super(), this._tooltipBox = null, this._timeout = 200, this._hideTimeout = null, this._hiddenTimeout = null;
super(), this._tooltipBox = null, this._timeout = 200, this._hideTimeout = null, this._hiddenTimeout = null, this._dataTipElem = null, this._observer = null;
}
connectedCallback() {
this.classList.add("relative", "block", "leading-none", "[&>*]:leading-normal");
const t = this.querySelector(".data-tip"), e = t ? t.innerHTML : "Tooltip";
t && t.classList.add("hidden"), this._tooltipBox = document.createElement("div"), this._tooltipBox.innerHTML = e, this._tooltipBox.className = [
this.classList.add("relative", "block", "leading-none", "[&>*]:leading-normal"), this._dataTipElem = this.querySelector(".data-tip");
const t = this._dataTipElem ? this._dataTipElem.innerHTML : "Tooltip";
this._dataTipElem && this._dataTipElem.classList.add("hidden"), this._tooltipBox = document.createElement("div"), this._tooltipBox.innerHTML = t, this._tooltipBox.className = [
"opacity-0",
"hidden",
"absolute",
@@ -5859,11 +5859,20 @@ class Nl extends HTMLElement {
"transition-all",
"duration-200",
"font-sans"
].join(" "), this.appendChild(this._tooltipBox), this._updatePosition(), this.addEventListener("mouseenter", () => this._showTooltip()), this.addEventListener("mouseleave", () => this._hideTooltip());
].join(" "), this.appendChild(this._tooltipBox), this._updatePosition(), this.addEventListener("mouseenter", () => this._showTooltip()), this.addEventListener("mouseleave", () => this._hideTooltip()), this._dataTipElem && (this._observer = new MutationObserver(() => {
this._tooltipBox && (this._tooltipBox.innerHTML = this._dataTipElem.innerHTML);
}), this._observer.observe(this._dataTipElem, {
childList: !0,
characterData: !0,
subtree: !0
}));
}
attributeChangedCallback(t, e, i) {
t === "position" && this._tooltipBox && this._updatePosition(), t === "timeout" && i && (this._timeout = parseInt(i) || 200);
}
disconnectedCallback() {
this._observer && this._observer.disconnect();
}
_showTooltip() {
clearTimeout(this._hideTimeout), clearTimeout(this._hiddenTimeout), this._tooltipBox.classList.remove("hidden"), setTimeout(() => {
this._tooltipBox.classList.remove("opacity-0"), this._tooltipBox.classList.add("opacity-100");
@@ -9128,6 +9137,8 @@ function Rt(s) {
console.log("Not a textarea element");
return;
}
if (s.dataset.noAutoresize === "true" || s.classList.contains("no-autoresize"))
return;
if (s.offsetParent === null) {
console.log("Textarea not visible");
return;
@@ -9150,7 +9161,7 @@ function Cd(s) {
console.warn("HookupTextareaAutoResize: Provided element is not a textarea.");
return;
}
vo() || s.addEventListener("input", () => {
s.dataset.noAutoresize === "true" || s.classList.contains("no-autoresize") || vo() || s.addEventListener("input", () => {
Rt(s);
});
}
@@ -9187,13 +9198,13 @@ function Rd(s) {
const t = document.querySelectorAll("textarea");
console.log("Found", t.length, "textareas");
for (const o of t)
console.log("Attaching input listener to:", o.name || o.id), o.addEventListener("input", function() {
o.dataset.noAutoresize === "true" || o.classList.contains("no-autoresize") || (console.log("Attaching input listener to:", o.name || o.id), o.addEventListener("input", function() {
console.log("Input event on textarea:", this.name || this.id), Rt(this);
});
}));
setTimeout(() => {
console.log("Running initial textarea resize on", t.length, "textareas");
for (const o of t)
Rt(o);
o.dataset.noAutoresize === "true" || o.classList.contains("no-autoresize") || Rt(o);
}, 200);
const e = document.querySelectorAll("textarea.no-enter");
for (const o of e)
@@ -9208,7 +9219,7 @@ function Rd(s) {
if (l instanceof HTMLElement) {
const d = l.matches("textarea") ? [l] : Array.from(l.querySelectorAll("textarea"));
for (const h of d)
h.offsetParent !== null && Rt(h);
h.dataset.noAutoresize === "true" || h.classList.contains("no-autoresize") || h.offsetParent !== null && Rt(h);
}
}
}).observe(s, {

File diff suppressed because one or more lines are too long

View File

@@ -132,7 +132,25 @@
<span data-role="contents-htmx-label">Eintrag wird geladen</span>
</div>
<input type="hidden" name="csrf_token" value="{{ $model.csrf_token }}" data-role="csrf-token" />
<div class="flex items-center justify-end gap-2 px-4">
<div class="flex items-center gap-3 px-4">
<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-sm 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-2 py-1 text-sm leading-5 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-100 px-3 py-1 text-sm font-semibold text-gray-700 hover:bg-stone-200">
Speichern
</button>
</form>
<div class="flex items-center gap-2">
<button type="button" class="resetbutton w-auto px-3 py-2 flex items-center gap-2" data-role="contents-collapse-all" data-state="expanded">
<i class="ri-arrow-up-s-line" data-role="contents-collapse-all-icon"></i>
<span data-role="contents-collapse-all-label">Alle Eintraege einklappen</span>
@@ -151,7 +169,8 @@
<span>Eintrag anlegen</span>
</button>
</div>
<div class="flex flex-col gap-1"
</div>
<div class="flex flex-col gap-0.5"
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"
@@ -263,7 +282,7 @@
}
};
const setEditSpacing = (active) => {
list.style.rowGap = active ? "0.75rem" : "";
list.style.rowGap = active ? "0.5rem" : "";
list.style.paddingTop = active ? "0.25rem" : "";
list.style.paddingBottom = active ? "0.25rem" : "";
};
@@ -317,6 +336,51 @@
}
};
const deleteContent = (item, dialog) => {
if (!item) {
return;
}
if (item.dataset.contentTemp === "true") {
dialog?.close();
removeItem(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,
},
});
dialog?.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(() => {
dialog?.close();
});
};
const performOrderSync = () => {
if (!list || !orderEndpoint || !csrfToken || isOrderSyncing) {
pendingOrderSync = true;
@@ -442,10 +506,16 @@
item.dataset.init = "true";
const editButton = item.querySelector("[data-role='content-edit-button']");
const deleteButtonView = item.querySelector("[data-role='content-delete-view']");
const deleteDialogView = item.querySelector("[data-role='content-delete-dialog-view']");
const deleteConfirmView = item.querySelector("[data-role='content-delete-confirm-view']");
const deleteCancelView = item.querySelector("[data-role='content-delete-cancel-view']");
const view = item.querySelector("[data-role='content-view']");
const collapseButton = item.querySelector("[data-role='content-collapse-toggle']");
const collapseIcon = item.querySelector("[data-role='content-collapse-icon']");
const collapseTooltip = item.querySelector("[data-role='content-collapse-tooltip']");
const collapsedSummary = item.querySelector("[data-role='content-collapsed-summary']");
const headerTitle = item.querySelector("[data-role='content-header-title']");
const viewBody = item.querySelector("[data-role='content-view-body']");
const header = item.querySelector("[data-content-header='true']");
@@ -457,6 +527,9 @@
item.classList.toggle("data-collapsed", collapsed);
viewBody.classList.toggle("hidden", collapsed);
collapsedSummary.classList.toggle("hidden", !collapsed);
if (headerTitle) {
headerTitle.classList.toggle("hidden", collapsed);
}
if (header) {
header.classList.toggle("bg-stone-100", collapsed);
header.classList.toggle("bg-stone-200", !collapsed);
@@ -465,6 +538,9 @@
collapseButton.setAttribute("aria-expanded", (!collapsed).toString());
collapseButton.setAttribute("aria-label", collapsed ? "Beitrag ausklappen" : "Beitrag einklappen");
}
if (collapseTooltip) {
collapseTooltip.textContent = collapsed ? "Ausklappen" : "Einklappen";
}
if (collapseIcon) {
collapseIcon.classList.toggle("ri-arrow-up-s-line", !collapsed);
collapseIcon.classList.toggle("ri-arrow-down-s-line", collapsed);
@@ -512,6 +588,32 @@
});
}
if (deleteButtonView && deleteDialogView) {
deleteButtonView.addEventListener("click", () => {
if (deleteDialogView.showModal) {
deleteDialogView.showModal();
} else {
deleteDialogView.setAttribute("open", "true");
}
});
}
if (deleteCancelView && deleteDialogView) {
deleteCancelView.addEventListener("click", () => {
deleteDialogView.close();
});
deleteDialogView.addEventListener("cancel", (event) => {
event.preventDefault();
deleteDialogView.close();
});
}
if (deleteConfirmView) {
deleteConfirmView.addEventListener("click", () => {
deleteContent(item, deleteDialogView);
});
}
const pendingEdit = item.dataset.pendingEdit === "true";
if (pendingEdit && item.querySelector("[data-role='content-edit']")) {
item.dataset.pendingEdit = "";
@@ -580,29 +682,7 @@
if (deleteConfirm) {
deleteConfirm.addEventListener("click", () => {
if (item.dataset.contentTemp === "true") {
deleteDialog?.close();
removeItem(item);
return;
}
const payload = new URLSearchParams();
payload.set("csrf_token", csrfToken);
payload.set("content_id", item.dataset.contentId || "");
fetch(deleteEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: payload.toString(),
})
.then((response) => {
if (response.redirected) {
window.location.assign(response.url);
} else {
window.location.reload();
}
})
.catch(() => null);
deleteContent(item, deleteDialog);
});
}
@@ -646,8 +726,10 @@
}
const viewBody = item.querySelector("[data-role='content-view-body']");
const collapsedSummary = item.querySelector("[data-role='content-collapsed-summary']");
const headerTitle = item.querySelector("[data-role='content-header-title']");
const collapseButton = item.querySelector("[data-role='content-collapse-toggle']");
const collapseIcon = item.querySelector("[data-role='content-collapse-icon']");
const collapseTooltip = item.querySelector("[data-role='content-collapse-tooltip']");
const header = item.querySelector("[data-content-header='true']");
if (!viewBody || !collapsedSummary) {
return;
@@ -656,6 +738,9 @@
item.classList.toggle("data-collapsed", collapsed);
viewBody.classList.toggle("hidden", collapsed);
collapsedSummary.classList.toggle("hidden", !collapsed);
if (headerTitle) {
headerTitle.classList.toggle("hidden", collapsed);
}
if (header) {
header.classList.toggle("bg-stone-100", collapsed);
header.classList.toggle("bg-stone-200", !collapsed);
@@ -664,6 +749,9 @@
collapseButton.setAttribute("aria-expanded", (!collapsed).toString());
collapseButton.setAttribute("aria-label", collapsed ? "Beitrag ausklappen" : "Beitrag einklappen");
}
if (collapseTooltip) {
collapseTooltip.textContent = collapsed ? "Ausklappen" : "Einklappen";
}
if (collapseIcon) {
collapseIcon.classList.toggle("ri-arrow-up-s-line", !collapsed);
collapseIcon.classList.toggle("ri-arrow-down-s-line", collapsed);
@@ -765,7 +853,7 @@
draggedItem = item;
item.dataset.dragging = "true";
draggedItem.classList.add("opacity-60");
list.style.rowGap = "0.75rem";
list.style.rowGap = "0.5rem";
list.style.paddingTop = "0.25rem";
list.style.paddingBottom = "0.25rem";
removeGaps();

View File

@@ -19,47 +19,68 @@
{{- $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 }}" data-content-id="{{ $contentID }}" data-collapsed="true">
<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-100 px-3 py-2 cursor-pointer hover:bg-stone-200 transition-colors duration-75" data-role="content-drag-handle" data-content-header="true" draggable="true" aria-label="Beitrag verschieben">
<div class="flex items-center gap-2 text-sm font-bold text-gray-800">
<button type="button" class="text-slate-600 rounded-xs px-2 py-1 text-sm transition-colors hover:bg-stone-100 {{ if $isNew }}hidden{{ end }}" data-role="content-collapse-toggle" aria-label="Beitrag einklappen" aria-expanded="true">
<div data-role="content-view" class="{{ if $openEdit }}hidden{{ end }} mt-1">
<div class="border border-slate-200 bg-stone-100 rounded-xs overflow-visible">
<div class="flex items-center justify-between gap-4 border-b border-slate-200 bg-stone-100 px-3 py-1 cursor-pointer hover:bg-stone-200 transition-colors duration-75 flex-nowrap whitespace-nowrap" data-role="content-drag-handle" data-content-header="true" draggable="true" aria-label="Beitrag verschieben">
<div class="flex items-center gap-2 text-sm font-bold text-gray-800 flex-1 min-w-0 flex-nowrap whitespace-nowrap">
<tool-tip position="top" class="!inline">
<div class="data-tip" data-role="content-collapse-tooltip">Einklappen</div>
<button type="button" class="text-slate-600 rounded-sm px-2 py-1 text-sm transition-colors hover:bg-stone-300 {{ if $isNew }}hidden{{ end }}" data-role="content-collapse-toggle" aria-label="Beitrag einklappen" aria-expanded="true">
<i class="ri-arrow-up-s-line" data-role="content-collapse-icon"></i>
</button>
</tool-tip>
<div class="flex items-center gap-1">
<button type="button" class="text-slate-600 rounded-xs px-2 py-1 text-sm transition-colors hover:bg-stone-100" data-role="content-move-up" aria-label="Beitrag nach oben">
<tool-tip position="top" class="!inline">
<div class="data-tip">Nach oben verschieben</div>
<button type="button" class="text-slate-600 rounded-sm px-2 py-1 text-sm transition-colors hover:bg-stone-300" data-role="content-move-up" aria-label="Beitrag nach oben">
<i class="ri-arrow-up-line"></i>
</button>
<button type="button" class="text-slate-600 rounded-xs px-2 py-1 text-sm transition-colors hover:bg-stone-100" data-role="content-move-down" aria-label="Beitrag nach unten">
</tool-tip>
<tool-tip position="top" class="!inline">
<div class="data-tip">Nach unten verschieben</div>
<button type="button" class="text-slate-600 rounded-sm px-2 py-1 text-sm transition-colors hover:bg-stone-300" data-role="content-move-down" aria-label="Beitrag nach unten">
<i class="ri-arrow-down-line"></i>
</button>
</tool-tip>
</div>
{{- if $content.MusenalmType -}}
<span class="flex flex-wrap gap-1 text-gray-700 font-normal">
<span class="flex flex-nowrap gap-1 text-gray-700 font-normal overflow-hidden">
{{- range $i, $t := $content.MusenalmType -}}
<span class="bg-slate-200 text-slate-900 px-1.5 py-0.5 rounded text-sm font-semibold shadow-sm" data-role="content-type-pill">{{- $t -}}</span>
{{- end -}}
</span>
{{- end -}}
<div class="hidden flex flex-wrap items-baseline gap-2 text-gray-800" data-role="content-collapsed-summary">
<div class="flex items-baseline gap-2 text-gray-800 min-w-0 flex-1 overflow-hidden flex-nowrap whitespace-nowrap" data-role="content-header-title">
{{- if $content.Extent -}}
<span class="bg-slate-200 text-slate-900 px-1.5 py-0.5 rounded text-sm font-semibold shadow-sm">S. {{- $content.Extent -}}</span>
<span class="bg-slate-200 text-slate-900 px-1.5 py-0.5 rounded text-xs font-semibold shadow-sm shrink-0">S. {{- $content.Extent -}}</span>
{{- end -}}
{{- if $content.PreferredTitle -}}
<span class="text-sm font-semibold">{{- $content.PreferredTitle -}}</span>
<span class="text-sm font-semibold truncate min-w-0 overflow-hidden">{{- $content.PreferredTitle -}}</span>
{{- else if $content.TitleStmt -}}
<span class="text-sm font-semibold italic">{{- $content.TitleStmt -}}</span>
<span class="text-sm font-semibold italic truncate min-w-0 overflow-hidden">{{- $content.TitleStmt -}}</span>
{{- end -}}
</div>
<div class="hidden flex items-baseline gap-2 text-gray-800 min-w-0 overflow-hidden flex-1 flex-nowrap whitespace-nowrap" data-role="content-collapsed-summary">
{{- if $content.Extent -}}
<span class="bg-slate-200 text-slate-900 px-1.5 py-0.5 rounded text-sm font-semibold shadow-sm shrink-0">S. {{- $content.Extent -}}</span>
{{- end -}}
{{- if $content.PreferredTitle -}}
<span class="text-sm font-semibold truncate min-w-0 overflow-hidden">{{- $content.PreferredTitle -}}</span>
{{- else if $content.TitleStmt -}}
<span class="text-sm font-semibold italic truncate min-w-0 overflow-hidden">{{- $content.TitleStmt -}}</span>
{{- end -}}
</div>
</div>
<div class="flex items-center gap-2">
<div class="flex items-center gap-2 flex-nowrap whitespace-nowrap shrink-0">
<span class="status-badge text-xs shadow-sm" data-status="{{ $content.EditState }}">
<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>
<tool-tip position="top" class="!inline">
<div class="data-tip">Bearbeiten</div>
<button
type="button"
class="resetbutton w-32 flex items-center gap-2 justify-center"
class="resetbutton w-9 h-9 flex items-center justify-center hover:bg-stone-300 rounded-sm"
data-role="content-edit-button"
data-loading-label="Eintrag wird geladen"
hx-boost="false"
@@ -67,19 +88,39 @@
hx-target="#{{ $editContainerID }}"
hx-swap="innerHTML">
<i class="ri-edit-2-line"></i>
<span>Bearbeiten</span>
<span class="sr-only">Bearbeiten</span>
</button>
</tool-tip>
<tool-tip position="top" class="!inline">
<div class="data-tip">Löschen</div>
<button
type="button"
class="resetbutton w-9 h-9 flex items-center justify-center text-red-700 hover:text-red-900 hover:bg-red-100 rounded-sm"
data-role="content-delete-view"
aria-label="Beitrag löschen">
<i class="ri-delete-bin-line"></i>
</button>
</tool-tip>
</div>
</div>
<dialog data-role="content-delete-dialog-view" 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-view">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-view">
Löschen
</button>
</div>
</div>
</dialog>
<div class="grid gap-2 grid-cols-[8rem_1fr] items-baseline px-3 py-2" data-role="content-view-body">
{{- if $content.Extent -}}
<div class="text-sm font-bold text-gray-700">Seite</div>
<div class="text-base">{{- $content.Extent -}}</div>
{{- end -}}
{{- if $content.TitleStmt -}}
<div class="text-sm font-bold text-gray-700">Titel</div>
<div class="text-base italic">{{- $content.TitleStmt -}}</div>
{{- end -}}
{{- if $content.IncipitStmt -}}
<div class="text-sm font-bold text-gray-700">Incipit</div>
<div class="text-base italic">{{ $content.IncipitStmt }}…</div>

View File

@@ -117,6 +117,15 @@
@apply italic;
}
.extent-inline textarea {
field-sizing: content;
resize: none;
max-height: 10rem;
overflow: hidden;
overflow-wrap: anywhere;
white-space: pre-wrap;
}
.dbform .submitbutton {
@apply w-full inline-flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-slate-700 hover:bg-slate-800 cursor-pointer focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500 active:bg-slate-900 transition-all duration-75;
}

View File

@@ -218,6 +218,9 @@ function TextareaAutoResize(textarea) {
console.log("Not a textarea element");
return;
}
if (textarea.dataset.noAutoresize === "true" || textarea.classList.contains("no-autoresize")) {
return;
}
// Skip if not visible
if (textarea.offsetParent === null) {
@@ -263,6 +266,9 @@ function HookupTextareaAutoResize(textarea) {
console.warn("HookupTextareaAutoResize: Provided element is not a textarea.");
return;
}
if (textarea.dataset.noAutoresize === "true" || textarea.classList.contains("no-autoresize")) {
return;
}
// If browser supports field-sizing, CSS handles it
if (supportsFieldSizing()) {
@@ -346,6 +352,9 @@ function FormLoad(form) {
// Attach resize handler to all textareas
for (const textarea of textareas) {
if (textarea.dataset.noAutoresize === "true" || textarea.classList.contains("no-autoresize")) {
continue;
}
console.log("Attaching input listener to:", textarea.name || textarea.id);
textarea.addEventListener("input", function () {
console.log("Input event on textarea:", this.name || this.id);
@@ -357,6 +366,9 @@ function FormLoad(form) {
setTimeout(() => {
console.log("Running initial textarea resize on", textareas.length, "textareas");
for (const textarea of textareas) {
if (textarea.dataset.noAutoresize === "true" || textarea.classList.contains("no-autoresize")) {
continue;
}
TextareaAutoResize(textarea);
}
}, 200);
@@ -382,6 +394,9 @@ function FormLoad(form) {
const textareasInTarget = target.matches("textarea") ? [target] : Array.from(target.querySelectorAll("textarea"));
for (const textarea of textareasInTarget) {
if (textarea.dataset.noAutoresize === "true" || textarea.classList.contains("no-autoresize")) {
continue;
}
// Only resize if now visible
if (textarea.offsetParent !== null) {
TextareaAutoResize(textarea);

View File

@@ -9,15 +9,17 @@ export class ToolTip extends HTMLElement {
this._timeout = 200;
this._hideTimeout = null;
this._hiddenTimeout = null;
this._dataTipElem = null;
this._observer = null;
}
connectedCallback() {
this.classList.add("relative", "block", "leading-none", "[&>*]:leading-normal");
const dataTipElem = this.querySelector(".data-tip");
const tipContent = dataTipElem ? dataTipElem.innerHTML : "Tooltip";
this._dataTipElem = this.querySelector(".data-tip");
const tipContent = this._dataTipElem ? this._dataTipElem.innerHTML : "Tooltip";
if (dataTipElem) {
dataTipElem.classList.add("hidden");
if (this._dataTipElem) {
this._dataTipElem.classList.add("hidden");
}
this._tooltipBox = document.createElement("div");
@@ -46,6 +48,19 @@ export class ToolTip extends HTMLElement {
this.addEventListener("mouseenter", () => this._showTooltip());
this.addEventListener("mouseleave", () => this._hideTooltip());
if (this._dataTipElem) {
this._observer = new MutationObserver(() => {
if (this._tooltipBox) {
this._tooltipBox.innerHTML = this._dataTipElem.innerHTML;
}
});
this._observer.observe(this._dataTipElem, {
childList: true,
characterData: true,
subtree: true,
});
}
}
attributeChangedCallback(name, oldValue, newValue) {
@@ -57,6 +72,12 @@ export class ToolTip extends HTMLElement {
}
}
disconnectedCallback() {
if (this._observer) {
this._observer.disconnect();
}
}
_showTooltip() {
clearTimeout(this._hideTimeout);
clearTimeout(this._hiddenTimeout);