BUGFIX: stresstest u select-vals

This commit is contained in:
Simon Martens
2026-01-23 16:18:05 +01:00
parent d8ed1aebe6
commit 0bd614712f
22 changed files with 1141 additions and 306 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -9,6 +9,7 @@
</head>
<body class="w-full min-h-full" id="body" hx-ext="response-targets" hx-boost="true">
{{ template "_global_notice" . }}
<div class="pb-12">
{{ block "body" . }}
<!-- Default app body... -->

View File

@@ -10,6 +10,7 @@
<body id="body" class="w-full min-h-full" hx-ext="response-targets" hx-boost="true">
<div class="flex flex-col min-h-screen w-full">
{{ template "_global_notice" . }}
<header class="container-normal bg-slate-100 " id="header">
{{ template "_menu" . }}
</header>

View File

@@ -0,0 +1,12 @@
<div
id="global-notice"
class="global-notice hidden"
role="status"
aria-live="polite"
aria-atomic="true"
data-state="">
<div class="global-notice-inner">
<i class="ri-loader-4-line spinning" aria-hidden="true"></i>
<span data-role="global-notice-text">Laden&#8230;</span>
</div>
</div>

View File

@@ -11,6 +11,7 @@
<body id="body" class="w-full text-lg" hx-ext="response-targets" hx-boost="true">
<div class="flex flex-col min-h-screen w-full">
{{ template "_global_notice" . }}
<header class="container-normal pb-0" id="header">
{{ block "_menu" . }}
<!-- Default app menu... -->

View File

@@ -495,10 +495,12 @@ type AlmanachResult struct {
</div>
</div>
<div id="series-section" class="rel-section-container">
{{- $hasNonPreferredSeries := false -}}
{{- if $model.result.Series -}}
{{- range $i, $s := $model.result.Series -}}
{{- $rel := index $model.result.EntriesSeries $s.Id -}}
{{- if and $rel (ne $rel.Type "Bevorzugter Reihentitel") -}}
{{- $hasNonPreferredSeries = true -}}
<div data-rel-row class="entries-series-row rel-row">
<div class="rel-grid">
<div data-rel-strike class="relation-strike rel-name-col">
@@ -542,7 +544,8 @@ type AlmanachResult struct {
</div>
{{- end -}}
{{- end -}}
{{- else -}}
{{- end -}}
{{- if not $hasNonPreferredSeries -}}
<div class="rel-empty-text">Keine Reihen verknüpft.</div>
{{- end -}}
</div>
@@ -716,7 +719,7 @@ type AlmanachResult struct {
<label for="entries_agents_new_type" class="sr-only">Beziehung</label>
<select data-role="relation-type-select" name="entries_agents_new_type" id="entries_agents_new_type" autocomplete="off" class="inputselect font-bold w-full">
{{- range $t := $model.agent_relations -}}
<option value="{{- $t -}}">{{- $t -}}</option>
<option value="{{- $t -}}" {{ if eq $t "Herausgeber:in" }}selected{{ end }}>{{- $t -}}</option>
{{- end -}}
</select>
</div>

View File

@@ -260,7 +260,7 @@
<div data-rel-strike class="relation-strike">
<select name="{{ $agentsPrefix }}_new_type" class="inputselect font-bold w-full">
{{- range $t := $agentRelations -}}
<option value="{{- $t -}}" {{ if eq $r.Type $t }}selected{{ end }}>{{- $t -}}</option>
<option value="{{- $t -}}" {{ if or (eq $r.Type $t) (and (eq $r.Type "") (eq $t "Autor:in")) }}selected{{ end }}>{{- $t -}}</option>
{{- end -}}
</select>
</div>
@@ -300,7 +300,7 @@
<label for="{{ $agentsPrefix }}_new_type" class="sr-only">Beziehung</label>
<select data-role="relation-type-select" name="{{ $agentsPrefix }}_new_type" id="{{ $agentsPrefix }}_new_type" autocomplete="off" class="inputselect font-bold w-full">
{{- range $t := $agentRelations -}}
<option value="{{- $t -}}">{{- $t -}}</option>
<option value="{{- $t -}}" {{ if eq $t "Autor:in" }}selected{{ end }}>{{- $t -}}</option>
{{- end -}}
</select>
</div>

View File

@@ -324,6 +324,168 @@ function DisconnectNoEnters(textarea) {
textarea.removeEventListener("keydown", NoEnters);
}
function InitGlobalHtmxNotice() {
if (!window.htmx) {
return;
}
const ensureNotice = () => {
let noticeEl = document.getElementById("global-notice");
if (!noticeEl) {
noticeEl = document.createElement("div");
noticeEl.id = "global-notice";
noticeEl.className = "global-notice hidden";
noticeEl.setAttribute("role", "status");
noticeEl.setAttribute("aria-live", "polite");
noticeEl.setAttribute("aria-atomic", "true");
noticeEl.dataset.state = "";
noticeEl.innerHTML = `
<div class="global-notice-inner">
<i class="ri-loader-4-line spinning" aria-hidden="true"></i>
<span data-role="global-notice-text">Laden&#8230;</span>
</div>
`;
document.body?.appendChild(noticeEl);
}
return noticeEl;
};
let notice = ensureNotice();
let textEl = notice ? notice.querySelector("[data-role='global-notice-text']") : null;
let pending = 0;
let errorTimeout = null;
const setNoticeState = (state, message) => {
notice = ensureNotice();
if (notice && !textEl) {
textEl = notice.querySelector("[data-role='global-notice-text']");
}
if (textEl && message) {
textEl.textContent = message;
}
if (notice && state) {
notice.dataset.state = state;
} else if (notice) {
notice.removeAttribute("data-state");
}
};
const showNotice = (state, message) => {
notice = ensureNotice();
if (!notice) {
return;
}
setNoticeState(state, message);
notice.classList.remove("hidden");
};
const hideNotice = () => {
notice = ensureNotice();
if (!notice) {
return;
}
notice.classList.add("hidden");
notice.removeAttribute("data-state");
};
const setBodyBusy = (busy) => {
const root = document.documentElement;
if (busy) {
if (root) {
root.dataset.htmxBusy = "true";
}
if (document.body) {
document.body.dataset.htmxBusy = "true";
}
} else {
if (root) {
delete root.dataset.htmxBusy;
}
if (document.body) {
delete document.body.dataset.htmxBusy;
}
}
};
const markElementBusy = (element, busy) => {
if (!element || !(element instanceof HTMLElement)) {
return;
}
if (busy) {
element.dataset.htmxBusy = "true";
element.setAttribute("aria-busy", "true");
if (element instanceof HTMLButtonElement && !element.disabled) {
element.dataset.htmxDisabled = "true";
element.disabled = true;
}
} else if (element.dataset.htmxBusy === "true") {
delete element.dataset.htmxBusy;
element.removeAttribute("aria-busy");
if (element instanceof HTMLButtonElement && element.dataset.htmxDisabled === "true") {
element.disabled = false;
delete element.dataset.htmxDisabled;
}
}
};
const clearErrorTimeout = () => {
if (errorTimeout) {
clearTimeout(errorTimeout);
errorTimeout = null;
}
};
document.addEventListener("htmx:beforeRequest", (event) => {
pending += 1;
clearErrorTimeout();
setBodyBusy(true);
showNotice("loading", "Laden...");
markElementBusy(event.detail?.elt, true);
});
document.addEventListener("htmx:afterRequest", (event) => {
markElementBusy(event.detail?.elt, false);
pending = Math.max(0, pending - 1);
if (pending === 0) {
setBodyBusy(false);
if (notice.dataset.state !== "error") {
hideNotice();
}
}
});
document.addEventListener("htmx:responseError", () => {
setBodyBusy(false);
showNotice("error", "Laden fehlgeschlagen.");
clearErrorTimeout();
errorTimeout = setTimeout(() => {
if (pending === 0) {
hideNotice();
} else {
showNotice("loading", "Laden...");
}
}, 2000);
});
document.addEventListener("htmx:sendError", () => {
setBodyBusy(false);
showNotice("error", "Verbindung fehlgeschlagen.");
clearErrorTimeout();
errorTimeout = setTimeout(() => {
if (pending === 0) {
hideNotice();
} else {
showNotice("loading", "Laden...");
}
}, 2000);
});
document.addEventListener("htmx:afterSwap", () => {
notice = ensureNotice();
if (notice && !textEl) {
textEl = notice.querySelector("[data-role='global-notice-text']");
}
});
}
function MutateObserve(mutations, observer) {
const needsJSResize = !supportsFieldSizing();
@@ -476,5 +638,6 @@ window.PathPlusQuery = PathPlusQuery;
window.HookupRBChange = HookupRBChange;
window.FormLoad = FormLoad;
window.TextareaAutoResize = TextareaAutoResize;
InitGlobalHtmxNotice();
export { FilterList, ScrollButton, AbbreviationTooltips, MultiSelectSimple, MultiSelectRole, ToolTip, PopupImage, TabList, FilterPill, ImageReel, IntLink, ItemsEditor, SingleSelectRemote, AlmanachEditPage, RelationsEditor, EditPage, FabMenu, LookupField };

View File

@@ -624,6 +624,30 @@
@apply !inline-block;
}
.global-notice {
@apply fixed right-6 bottom-6 z-50 hidden;
}
.global-notice-inner {
@apply flex items-center gap-2 rounded-md border border-slate-200 bg-white/95 px-3 py-2 text-sm font-semibold text-gray-700 shadow-lg backdrop-blur;
}
.global-notice[data-state="error"] .global-notice-inner {
@apply border-red-200 bg-red-50 text-red-800;
}
html[data-htmx-busy="true"] .global-notice,
body[data-htmx-busy="true"] .global-notice {
@apply block;
}
html[data-htmx-busy="true"],
body[data-htmx-busy="true"],
[data-htmx-busy="true"] {
pointer-events: none;
opacity: 0.6;
}
.tab-list-head[aria-pressed="true"] {
@apply !text-slate-900 bg-stone-50;
}