some list things; image uplaod

This commit is contained in:
Simon Martens
2026-01-21 21:12:05 +01:00
parent bd4d6571e0
commit 1aa24b97cc
9 changed files with 1582 additions and 1193 deletions

View File

@@ -223,7 +223,6 @@
{{- if $i }},{{ end }}
"{{ $content.Id }}": {
extent: "{{ $content.Extent }}",
pagination: "{{ $content.MusenalmPagination }}",
musenalmType: [{{- range $j, $t := $content.MusenalmType -}}{{- if $j }},{{ end }}"{{ $t }}"{{- end -}}],
preferredTitle: "{{ $content.PreferredTitle }}",
titleStmt: "{{ $content.TitleStmt }}",
@@ -296,7 +295,6 @@
// Field labels for display
const fieldLabels = {
extent: 'Seite',
pagination: 'Paginierung',
musenalmType: 'Typ',
titleStmt: 'Titel',
subtitleStmt: 'Untertitel',
@@ -311,6 +309,51 @@
annotation: 'Anmerkung'
};
const getMarkTargets = () => {
if (!list) {
return [];
}
const visibleItems = Array.from(list.querySelectorAll("[data-role='content-item']")).filter((item) => item.style.display !== "none");
const targets = [];
visibleItems.forEach((item) => {
targets.push(...item.querySelectorAll(".content-search-text"));
});
return targets;
};
const updateMarks = () => {
if (typeof Mark !== "function") {
return;
}
const terms = [];
const rawQuery = (filterInput.value || "").trim();
const rawPage = (filterPage?.value || "").trim();
const rawType = (filterType?.value || "").trim();
if (rawQuery) {
terms.push(rawQuery);
}
if (rawPage) {
terms.push(rawPage);
}
if (rawType) {
terms.push(rawType);
}
const targets = getMarkTargets();
if (targets.length === 0) {
return;
}
const markInstance = new Mark(targets);
markInstance.unmark({
done: () => {
if (terms.length > 0) {
markInstance.mark(terms, {
separateWordSearch: true,
});
}
},
});
};
const performFilter = () => {
const query = normalizeText(filterInput.value);
const pageQuery = normalizeText(filterPage?.value || '');
@@ -401,8 +444,6 @@
};
// Check text search fields (excluding language, status, preferredTitle)
checkField('extent', contentData.extent, fieldLabels.extent);
checkField('pagination', contentData.pagination, fieldLabels.pagination);
checkArrayField('musenalmType', contentData.musenalmType, fieldLabels.musenalmType);
checkField('titleStmt', contentData.titleStmt, fieldLabels.titleStmt);
checkField('subtitleStmt', contentData.subtitleStmt, fieldLabels.subtitleStmt);
@@ -463,6 +504,8 @@
? `${totalCount} Einträge`
: `${visibleCount} von ${totalCount} Einträgen`;
}
updateMarks();
};
const debounceFilter = () => {
@@ -522,6 +565,15 @@
}
syncIndicator.classList.toggle("hidden", !active);
};
const setDraggingState = (active) => {
if (active) {
document.body.dataset.dragging = "true";
} else {
delete document.body.dataset.dragging;
}
window.__toolTipDragging = active;
window.dispatchEvent(new CustomEvent("contentsdragging", { detail: { active } }));
};
// Shared delete dialog
const deleteDialog = document.getElementById("content-delete-dialog");
const deleteDialogTitle = document.getElementById("content-delete-dialog-title");
@@ -665,8 +717,59 @@
if (list.dataset.pageInit !== "true") {
list.dataset.pageInit = "true";
let draggedItem = null;
let pointerDrag = null;
let lastDragOverTime = 0;
const DRAG_THROTTLE_MS = 100;
const startPointerDrag = (event) => {
const handle = event.target.closest("[data-role='content-drag-handle']");
if (!handle || event.button !== 0) {
return;
}
const item = handle.closest("[data-role='content-item']");
if (!item) {
return;
}
event.preventDefault();
pointerDrag = {
item,
pointerId: event.pointerId,
};
item.dataset.dragging = "true";
item.classList.add("opacity-60");
setDraggingState(true);
if (handle.setPointerCapture) {
handle.setPointerCapture(event.pointerId);
}
};
const movePointerDrag = (event) => {
if (!pointerDrag || event.pointerId !== pointerDrag.pointerId) {
return;
}
const item = pointerDrag.item;
const targetItem = document.elementFromPoint(event.clientX, event.clientY)?.closest("[data-role='content-item']");
if (!targetItem || targetItem === item) {
return;
}
const rect = targetItem.getBoundingClientRect();
const before = event.clientY - rect.top < rect.height / 2;
if (before) {
targetItem.before(item);
} else {
targetItem.after(item);
}
};
const endPointerDrag = (event) => {
if (!pointerDrag || event.pointerId !== pointerDrag.pointerId) {
return;
}
pointerDrag.item.classList.remove("opacity-60");
pointerDrag.item.dataset.dragging = "";
pointerDrag = null;
setDraggingState(false);
syncOrder();
};
list.addEventListener("click", (event) => {
const moveUp = event.target.closest("[data-role='content-move-up']");
const moveDown = event.target.closest("[data-role='content-move-down']");
@@ -699,6 +802,10 @@
});
list.addEventListener("dragstart", (event) => {
if (pointerDrag) {
event.preventDefault();
return;
}
if (event.target.closest("[data-role='content-move-up']") || event.target.closest("[data-role='content-move-down']")) {
return;
}
@@ -717,6 +824,7 @@
draggedItem = item;
item.dataset.dragging = "true";
draggedItem.classList.add("opacity-60");
setDraggingState(true);
event.dataTransfer.effectAllowed = "move";
event.dataTransfer.setData("text/plain", "move");
});
@@ -745,8 +853,14 @@
draggedItem.dataset.dragging = "";
}
draggedItem = null;
setDraggingState(false);
syncOrder();
});
list.addEventListener("pointerdown", startPointerDrag);
list.addEventListener("pointermove", movePointerDrag);
list.addEventListener("pointerup", endPointerDrag);
list.addEventListener("pointercancel", endPointerDrag);
}
};

View File

@@ -209,6 +209,12 @@
data-initial-values='[{{- range $i, $lang := $model.content.Language -}}{{- if $i }},{{ end -}}{{ printf "%q" $lang }}{{- end -}}]'>
</multi-select-simple>
</div>
{{- template "_content_images_panel" (Dict
"content" $model.content
"entry" $model.result.Entry
"csrf_token" $model.csrf_token
"is_new" false
) -}}
</div>
</div>

View File

@@ -4,30 +4,18 @@
{{- $isNew := index . "is_new" -}}
{{- if or $content.ImagePaths (not $isNew) -}}
<div class="w-full md:w-56 lg:w-72 shrink-0" data-role="content-images-panel">
<div class="flex flex-wrap items-start gap-2">
{{- if $content.ImagePaths -}}
<content-images
data-images='[{{- range $i, $scan := $content.ImagePaths -}}{{- if $i }},{{ end -}}{{ printf "%q" $scan }}{{- end -}}]'
data-files='[{{- range $i, $scan := $content.Scans -}}{{- if $i }},{{ end -}}{{ printf "%q" $scan }}{{- end -}}]'
data-delete-endpoint="/almanach/{{ $entry.MusenalmID }}/contents/scan/delete"
data-content-id="{{ $content.Id }}"
data-csrf-token="{{ $csrf }}">
</content-images>
{{- end -}}
<div class="w-full" data-role="content-images-panel">
<div class="flex flex-col items-start gap-2">
<content-images
class="w-full"
data-images='[{{- range $i, $scan := $content.ImagePaths -}}{{- if $i }},{{ end -}}{{ printf "%q" $scan }}{{- end -}}]'
data-files='[{{- range $i, $scan := $content.Scans -}}{{- if $i }},{{ end -}}{{ printf "%q" $scan }}{{- end -}}]'
data-delete-endpoint="/almanach/{{ $entry.MusenalmID }}/contents/scan/delete"
data-content-id="{{ $content.Id }}"
data-csrf-token="{{ $csrf }}">
</content-images>
{{- if not $isNew -}}
<form
class="flex"
method="POST"
action="/almanach/{{ $entry.MusenalmID }}/contents/upload"
hx-post="/almanach/{{ $entry.MusenalmID }}/contents/upload"
hx-trigger="change"
hx-target="closest [data-role='content-images-panel']"
hx-swap="outerHTML"
hx-encoding="multipart/form-data"
data-loading-label="Digitalisat wird hochgeladen">
<input type="hidden" name="csrf_token" value="{{ $csrf }}" />
<input type="hidden" name="content_id" value="{{ $content.Id }}" />
<div class="flex" data-role="content-images-upload">
<label
for="content-{{ $content.Id }}-scan-upload"
class="flex h-28 w-28 items-center justify-center rounded-xs border-2 border-dashed border-slate-300 bg-stone-50 text-lg font-semibold text-slate-600 transition hover:border-slate-400 hover:text-slate-800"
@@ -40,8 +28,12 @@
name="scans"
multiple
accept="image/*"
class="sr-only" />
</form>
class="sr-only"
data-role="content-images-upload-input"
data-upload-endpoint="/almanach/{{ $entry.MusenalmID }}/contents/upload"
data-content-id="{{ $content.Id }}"
data-csrf-token="{{ $csrf }}" />
</div>
{{- end -}}
</div>
</div>

View File

@@ -37,21 +37,25 @@
</tool-tip>
</div>
{{- 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" data-role="content-page-pill">S. {{- $content.Extent -}}</span>
<span class="content-search-text bg-slate-200 text-slate-900 px-1.5 py-0.5 rounded text-sm font-semibold shadow-sm shrink-0" data-role="content-page-pill">S. {{- $content.Extent -}}</span>
{{- end -}}
{{- if $content.MusenalmType -}}
<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-base font-semibold shadow-sm" data-role="content-type-pill">{{- $t -}}</span>
<span class="content-search-text bg-slate-200 text-slate-900 px-1.5 py-0.5 rounded text-base font-semibold shadow-sm" data-role="content-type-pill">{{- $t -}}</span>
{{- end -}}
</span>
{{- end -}}
<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.PreferredTitle -}}
<span class="text-lg font-normal truncate min-w-0 overflow-hidden" data-role="content-header-title-text">{{- $content.PreferredTitle -}}</span>
{{- else if $content.TitleStmt -}}
<span class="text-lg font-normal italic truncate min-w-0 overflow-hidden" data-role="content-header-title-text">{{- $content.TitleStmt -}}</span>
{{- end -}}
<a
href="/almanach/{{ $entry.MusenalmID }}/contents/{{ $content.MusenalmID }}/edit"
class="no-underline hover:text-slate-900 cursor-pointer">
{{- if $content.PreferredTitle -}}
<span class="content-search-text text-lg font-normal truncate min-w-0 overflow-hidden" data-role="content-header-title-text">{{- $content.PreferredTitle -}}</span>
{{- else if $content.TitleStmt -}}
<span class="content-search-text text-lg font-normal italic truncate min-w-0 overflow-hidden" data-role="content-header-title-text">{{- $content.TitleStmt -}}</span>
{{- end -}}
</a>
</div>
</div>
<div class="flex items-center gap-2 flex-nowrap whitespace-nowrap shrink-0">
@@ -79,7 +83,7 @@
</button>
</div>
</div>
<div data-role="content-match-display" class="hidden px-2 py-1 text-sm text-gray-600 bg-amber-50 border-l-2 border-amber-400"></div>
<div data-role="content-match-display" class="content-search-text hidden px-2 py-1 text-sm text-gray-600 bg-amber-50 border-l-2 border-amber-400"></div>
</div>
</div>
</div>