mirror of
https://github.com/Theodor-Springmann-Stiftung/musenalm.git
synced 2026-02-04 02:25:30 +00:00
+density on list
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user