mirror of
https://github.com/Theodor-Springmann-Stiftung/musenalm.git
synced 2026-02-04 10:35:30 +00:00
+Image upload
This commit is contained in:
@@ -183,17 +183,98 @@ func (p *AlmanachContentsEditPage) renderError(engine *templating.Engine, app co
|
|||||||
return engine.Response200(e, p.Template, data, p.Layout)
|
return engine.Response200(e, p.Template, data, p.Layout)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *AlmanachContentsEditPage) renderItemError(engine *templating.Engine, app core.App, e *core.RequestEvent, contentID string, message string) error {
|
||||||
|
id := e.Request.PathValue("id")
|
||||||
|
req := templating.NewRequest(e)
|
||||||
|
data := make(map[string]any)
|
||||||
|
result, err := NewAlmanachEditResult(app, id, BeitraegeFilterParameters{})
|
||||||
|
if err != nil {
|
||||||
|
return engine.Response404(e, err, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
contents, err := dbmodels.Contents_IDs(app, []any{contentID})
|
||||||
|
if err != nil || len(contents) == 0 {
|
||||||
|
return p.renderError(engine, app, e, message)
|
||||||
|
}
|
||||||
|
content := contents[0]
|
||||||
|
if content.Entry() != result.Entry.Id {
|
||||||
|
return p.renderError(engine, app, e, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
entryContents, err := dbmodels.Contents_Entry(app, result.Entry.Id)
|
||||||
|
if err == nil && len(entryContents) > 1 {
|
||||||
|
sort.Slice(entryContents, func(i, j int) bool {
|
||||||
|
if entryContents[i].Numbering() == entryContents[j].Numbering() {
|
||||||
|
return entryContents[i].Id < entryContents[j].Id
|
||||||
|
}
|
||||||
|
return entryContents[i].Numbering() < entryContents[j].Numbering()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
var prevContent *dbmodels.Content
|
||||||
|
var nextContent *dbmodels.Content
|
||||||
|
if len(entryContents) > 0 {
|
||||||
|
for i, c := range entryContents {
|
||||||
|
if c.Id != content.Id {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if i > 0 {
|
||||||
|
prevContent = entryContents[i-1]
|
||||||
|
}
|
||||||
|
if i < len(entryContents)-1 {
|
||||||
|
nextContent = entryContents[i+1]
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
agentsMap, contentAgentsMap, err := dbmodels.AgentsForContents(app, []*dbmodels.Content{content})
|
||||||
|
if err != nil {
|
||||||
|
agentsMap = map[string]*dbmodels.Agent{}
|
||||||
|
contentAgentsMap = map[string][]*dbmodels.RContentsAgents{}
|
||||||
|
}
|
||||||
|
|
||||||
|
data["result"] = result
|
||||||
|
data["csrf_token"] = req.Session().Token
|
||||||
|
data["content"] = content
|
||||||
|
data["content_id"] = content.Id
|
||||||
|
data["content_types"] = dbmodels.CONTENT_TYPE_VALUES
|
||||||
|
data["musenalm_types"] = dbmodels.MUSENALM_TYPE_VALUES
|
||||||
|
data["pagination_values"] = paginationValuesSorted()
|
||||||
|
data["agent_relations"] = dbmodels.AGENT_RELATIONS
|
||||||
|
data["agents"] = agentsMap
|
||||||
|
data["content_agents"] = contentAgentsMap[content.Id]
|
||||||
|
data["prev_content"] = prevContent
|
||||||
|
data["next_content"] = nextContent
|
||||||
|
data["error"] = message
|
||||||
|
|
||||||
|
return engine.Response200(e, TEMPLATE_ALMANACH_CONTENTS_ITEM_EDIT, data, p.Layout)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *AlmanachContentsEditPage) POSTSave(engine *templating.Engine, app core.App) HandleFunc {
|
func (p *AlmanachContentsEditPage) POSTSave(engine *templating.Engine, app core.App) HandleFunc {
|
||||||
return func(e *core.RequestEvent) error {
|
return func(e *core.RequestEvent) error {
|
||||||
id := e.Request.PathValue("id")
|
id := e.Request.PathValue("id")
|
||||||
req := templating.NewRequest(e)
|
req := templating.NewRequest(e)
|
||||||
|
|
||||||
if err := e.Request.ParseForm(); err != nil {
|
if e.Request.MultipartForm == nil {
|
||||||
return p.renderError(engine, app, e, err.Error())
|
if err := e.Request.ParseMultipartForm(router.DefaultMaxMemory); err != nil {
|
||||||
|
if e.Request.MultipartForm == nil {
|
||||||
|
if err := e.Request.ParseForm(); err != nil {
|
||||||
|
return p.renderError(engine, app, e, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contentID := strings.TrimSpace(e.Request.FormValue("content_id"))
|
||||||
|
renderError := func(message string) error {
|
||||||
|
if contentID != "" {
|
||||||
|
return p.renderItemError(engine, app, e, contentID, message)
|
||||||
|
}
|
||||||
|
return p.renderError(engine, app, e, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := req.CheckCSRF(e.Request.FormValue("csrf_token")); err != nil {
|
if err := req.CheckCSRF(e.Request.FormValue("csrf_token")); err != nil {
|
||||||
return p.renderError(engine, app, e, err.Error())
|
return renderError(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
entry, err := dbmodels.Entries_MusenalmID(app, id)
|
entry, err := dbmodels.Entries_MusenalmID(app, id)
|
||||||
@@ -234,6 +315,19 @@ func (p *AlmanachContentsEditPage) POSTSave(engine *templating.Engine, app core.
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uploadedScans, _ := e.FindUploadedFiles(dbmodels.SCAN_FIELD)
|
||||||
|
deleteScans := valuesForKey(e.Request.PostForm, "scans_delete")
|
||||||
|
targetContentID := contentID
|
||||||
|
if targetContentID == "" && len(contentInputs) == 1 {
|
||||||
|
for id := range contentInputs {
|
||||||
|
targetContentID = id
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if contentID == "" {
|
||||||
|
contentID = targetContentID
|
||||||
|
}
|
||||||
|
|
||||||
var updatedContents []*dbmodels.Content
|
var updatedContents []*dbmodels.Content
|
||||||
if err := app.RunInTransaction(func(tx core.App) error {
|
if err := app.RunInTransaction(func(tx core.App) error {
|
||||||
if len(orderMap) > 0 {
|
if len(orderMap) > 0 {
|
||||||
@@ -309,12 +403,40 @@ func (p *AlmanachContentsEditPage) POSTSave(engine *templating.Engine, app core.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if targetContentID != "" && (len(uploadedScans) > 0 || len(deleteScans) > 0) {
|
||||||
|
record, err := tx.FindRecordById(dbmodels.CONTENTS_TABLE, targetContentID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
content := dbmodels.NewContent(record)
|
||||||
|
if content.Entry() != entry.Id {
|
||||||
|
return fmt.Errorf("Beitrag gehört zu einem anderen Band.")
|
||||||
|
}
|
||||||
|
if len(uploadedScans) > 0 {
|
||||||
|
content.Set(dbmodels.SCAN_FIELD+"+", uploadedScans)
|
||||||
|
}
|
||||||
|
if len(deleteScans) > 0 {
|
||||||
|
for _, scan := range deleteScans {
|
||||||
|
scan = strings.TrimSpace(scan)
|
||||||
|
if scan == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
content.Set(dbmodels.SCAN_FIELD+"-", scan)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if user != nil {
|
||||||
|
content.SetEditor(user.Id)
|
||||||
|
}
|
||||||
|
if err := tx.Save(content); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
updatedContents = append(updatedContents, contents...)
|
updatedContents = append(updatedContents, contents...)
|
||||||
updatedContents = append(updatedContents, created...)
|
updatedContents = append(updatedContents, created...)
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
app.Logger().Error("Failed to save contents", "entry_id", entry.Id, "error", err)
|
app.Logger().Error("Failed to save contents", "entry_id", entry.Id, "error", err)
|
||||||
return p.renderError(engine, app, e, err.Error())
|
return renderError(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(updatedContents) == 0 {
|
if len(updatedContents) == 0 {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -150,6 +150,8 @@
|
|||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
class="w-full dbform"
|
class="w-full dbform"
|
||||||
method="POST"
|
method="POST"
|
||||||
|
enctype="multipart/form-data"
|
||||||
|
hx-boost="false"
|
||||||
action="/almanach/{{ $model.result.Entry.MusenalmID }}/contents/edit">
|
action="/almanach/{{ $model.result.Entry.MusenalmID }}/contents/edit">
|
||||||
<input type="hidden" name="csrf_token" value="{{ $model.csrf_token }}" />
|
<input type="hidden" name="csrf_token" value="{{ $model.csrf_token }}" />
|
||||||
|
|
||||||
@@ -343,5 +345,65 @@
|
|||||||
deleteDialog.close();
|
deleteDialog.close();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const form = document.querySelector("form.dbform");
|
||||||
|
const uploadInput = document.querySelector("[data-role='content-images-upload-input']");
|
||||||
|
const userMessage = document.getElementById("user-message");
|
||||||
|
if (form && uploadInput && userMessage) {
|
||||||
|
form.addEventListener("submit", async (event) => {
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
const files = Array.from(uploadInput.files || []);
|
||||||
|
if (files.length > 0) {
|
||||||
|
const hasInvalid = files.some((file) => !file.type || !file.type.startsWith("image/"));
|
||||||
|
if (hasInvalid) {
|
||||||
|
event.preventDefault();
|
||||||
|
userMessage.innerHTML = `
|
||||||
|
<div class="text-red-800 text-sm mt-2 rounded-xs bg-red-200 p-2 font-bold border-red-700 shadow border mb-3">
|
||||||
|
<i class="ri-error-warning-fill"></i> Bitte nur Bilddateien auswählen.
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
const payload = new FormData(form);
|
||||||
|
if (payload.has("scans")) {
|
||||||
|
payload.delete("scans");
|
||||||
|
}
|
||||||
|
const imagesComponent = document.querySelector("content-images");
|
||||||
|
if (imagesComponent && typeof imagesComponent.getPendingFiles === "function") {
|
||||||
|
imagesComponent.getPendingFiles().forEach((file) => {
|
||||||
|
payload.append("scans", file);
|
||||||
|
});
|
||||||
|
if (typeof imagesComponent.getPendingDeletes === "function") {
|
||||||
|
imagesComponent.getPendingDeletes().forEach((fileName) => {
|
||||||
|
payload.append("scans_delete[]", fileName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const response = await fetch(form.action, {
|
||||||
|
method: form.method || "POST",
|
||||||
|
body: payload,
|
||||||
|
credentials: "same-origin",
|
||||||
|
});
|
||||||
|
if (response.redirected && response.url) {
|
||||||
|
window.location.assign(response.url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!response.ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const html = await response.text();
|
||||||
|
if (!html) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const doc = new DOMParser().parseFromString(html, "text/html");
|
||||||
|
const nextMessage = doc.getElementById("user-message");
|
||||||
|
if (nextMessage) {
|
||||||
|
userMessage.innerHTML = nextMessage.innerHTML;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -4,8 +4,11 @@
|
|||||||
{{- $isNew := index . "is_new" -}}
|
{{- $isNew := index . "is_new" -}}
|
||||||
|
|
||||||
{{- if or $content.ImagePaths (not $isNew) -}}
|
{{- if or $content.ImagePaths (not $isNew) -}}
|
||||||
<div class="w-full" data-role="content-images-panel">
|
<div class="w-full inputwrapper" data-role="content-images-panel">
|
||||||
<div class="flex flex-col items-start gap-2">
|
<div class="inputlabelrow">
|
||||||
|
<label class="inputlabel">Scans</label>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-start gap-2 p-2">
|
||||||
<content-images
|
<content-images
|
||||||
class="w-full"
|
class="w-full"
|
||||||
data-images='[{{- range $i, $scan := $content.ImagePaths -}}{{- if $i }},{{ end -}}{{ printf "%q" $scan }}{{- end -}}]'
|
data-images='[{{- range $i, $scan := $content.ImagePaths -}}{{- if $i }},{{ end -}}{{ printf "%q" $scan }}{{- end -}}]'
|
||||||
@@ -15,7 +18,8 @@
|
|||||||
data-csrf-token="{{ $csrf }}">
|
data-csrf-token="{{ $csrf }}">
|
||||||
</content-images>
|
</content-images>
|
||||||
{{- if not $isNew -}}
|
{{- if not $isNew -}}
|
||||||
<div class="flex" data-role="content-images-upload">
|
<div class="hidden" data-role="content-images-upload">
|
||||||
|
<input type="hidden" name="content_id" value="{{ $content.Id }}" />
|
||||||
<label
|
<label
|
||||||
for="content-{{ $content.Id }}-scan-upload"
|
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"
|
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"
|
||||||
@@ -30,7 +34,6 @@
|
|||||||
accept="image/*"
|
accept="image/*"
|
||||||
class="sr-only"
|
class="sr-only"
|
||||||
data-role="content-images-upload-input"
|
data-role="content-images-upload-input"
|
||||||
data-upload-endpoint="/almanach/{{ $entry.MusenalmID }}/contents/upload"
|
|
||||||
data-content-id="{{ $content.Id }}"
|
data-content-id="{{ $content.Id }}"
|
||||||
data-csrf-token="{{ $csrf }}" />
|
data-csrf-token="{{ $csrf }}" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ export class ContentImages extends HTMLElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.dataset.init = "true";
|
this.dataset.init = "true";
|
||||||
|
this._pendingFiles = [];
|
||||||
|
this._pendingUrls = [];
|
||||||
|
this._pendingDeletes = new Set();
|
||||||
|
|
||||||
this._wireUpload();
|
this._wireUpload();
|
||||||
|
|
||||||
@@ -76,11 +79,6 @@ export class ContentImages extends HTMLElement {
|
|||||||
|
|
||||||
const normalized = normalizeImages(images, files);
|
const normalized = normalizeImages(images, files);
|
||||||
|
|
||||||
if (!Array.isArray(normalized) || normalized.length === 0) {
|
|
||||||
this.classList.add("hidden");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._render(normalized);
|
this._render(normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,53 +93,30 @@ export class ContentImages extends HTMLElement {
|
|||||||
}
|
}
|
||||||
uploadInput.dataset.bound = "true";
|
uploadInput.dataset.bound = "true";
|
||||||
uploadInput.addEventListener("change", () => {
|
uploadInput.addEventListener("change", () => {
|
||||||
this._uploadFiles(uploadInput, panel);
|
this._setPendingFiles(Array.from(uploadInput.files || []));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_uploadFiles(input, panel) {
|
_setPendingFiles(files) {
|
||||||
const endpoint = input.getAttribute("data-upload-endpoint") || "";
|
this._clearPendingPreviews();
|
||||||
const contentId = input.getAttribute("data-content-id") || "";
|
this._pendingFiles = Array.isArray(files) ? files : [];
|
||||||
const csrfToken = input.getAttribute("data-csrf-token") || "";
|
this._pendingUrls = this._pendingFiles.map((file) => URL.createObjectURL(file));
|
||||||
const files = Array.from(input.files || []);
|
this._render(this._currentImages || []);
|
||||||
if (!endpoint || !contentId || !csrfToken || files.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const payload = new FormData();
|
|
||||||
payload.append("csrf_token", csrfToken);
|
|
||||||
payload.append("content_id", contentId);
|
|
||||||
files.forEach((file) => payload.append("scans", file));
|
|
||||||
fetch(endpoint, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"HX-Request": "true",
|
|
||||||
},
|
|
||||||
body: payload,
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
if (!response.ok) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return response.text();
|
|
||||||
})
|
|
||||||
.then((html) => {
|
|
||||||
if (!html || !panel) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._applyServerResponse(html, panel);
|
|
||||||
})
|
|
||||||
.catch(() => null)
|
|
||||||
.finally(() => {
|
|
||||||
input.value = "";
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_render(images) {
|
_render(images) {
|
||||||
|
this._currentImages = images;
|
||||||
this.classList.add("block");
|
this.classList.add("block");
|
||||||
this.style.display = "block";
|
this.style.display = "block";
|
||||||
this.style.width = "100%";
|
this.style.width = "100%";
|
||||||
const list = this._ensureList();
|
const list = this._ensureList();
|
||||||
list.innerHTML = "";
|
const uploadProxy = this._ensureUploadProxy();
|
||||||
|
if (uploadProxy && uploadProxy.parentElement === list) {
|
||||||
|
uploadProxy.remove();
|
||||||
|
}
|
||||||
|
list.querySelectorAll("[data-role='content-images-item'], [data-role='content-images-pending']").forEach((node) => {
|
||||||
|
node.remove();
|
||||||
|
});
|
||||||
|
|
||||||
const deleteEndpoint = this.getAttribute("data-delete-endpoint") || "";
|
const deleteEndpoint = this.getAttribute("data-delete-endpoint") || "";
|
||||||
const contentId = this.getAttribute("data-content-id") || "";
|
const contentId = this.getAttribute("data-content-id") || "";
|
||||||
@@ -151,9 +126,15 @@ export class ContentImages extends HTMLElement {
|
|||||||
images.forEach((image, index) => {
|
images.forEach((image, index) => {
|
||||||
const wrapper = document.createElement("div");
|
const wrapper = document.createElement("div");
|
||||||
wrapper.className = "group relative";
|
wrapper.className = "group relative";
|
||||||
|
wrapper.dataset.role = "content-images-item";
|
||||||
|
const isPendingDelete = this._pendingDeletes.has(image.name);
|
||||||
|
if (isPendingDelete) {
|
||||||
|
wrapper.classList.add("content-image-pending");
|
||||||
|
}
|
||||||
const button = document.createElement("button");
|
const button = document.createElement("button");
|
||||||
button.type = "button";
|
button.type = "button";
|
||||||
button.className = [
|
button.className = [
|
||||||
|
"relative",
|
||||||
"rounded",
|
"rounded",
|
||||||
"border",
|
"border",
|
||||||
"border-slate-200",
|
"border-slate-200",
|
||||||
@@ -166,6 +147,10 @@ export class ContentImages extends HTMLElement {
|
|||||||
].join(" ");
|
].join(" ");
|
||||||
button.dataset.imageUrl = image.url;
|
button.dataset.imageUrl = image.url;
|
||||||
button.dataset.imageIndex = String(index);
|
button.dataset.imageIndex = String(index);
|
||||||
|
if (isPendingDelete) {
|
||||||
|
button.setAttribute("aria-disabled", "true");
|
||||||
|
button.classList.add("content-image-pending-button");
|
||||||
|
}
|
||||||
|
|
||||||
const img = document.createElement("img");
|
const img = document.createElement("img");
|
||||||
img.src = buildThumbUrl(image.url, THUMB_PARAM);
|
img.src = buildThumbUrl(image.url, THUMB_PARAM);
|
||||||
@@ -184,32 +169,33 @@ export class ContentImages extends HTMLElement {
|
|||||||
"right-1",
|
"right-1",
|
||||||
"top-1",
|
"top-1",
|
||||||
"hidden",
|
"hidden",
|
||||||
"h-8",
|
|
||||||
"w-8",
|
|
||||||
"rounded-full",
|
"rounded-full",
|
||||||
"border",
|
"border",
|
||||||
"border-red-200",
|
"border-red-200",
|
||||||
"bg-white/90",
|
"bg-white/90",
|
||||||
"flex",
|
"px-2",
|
||||||
"items-center",
|
"py-1",
|
||||||
"justify-center",
|
"text-xs",
|
||||||
|
"font-semibold",
|
||||||
"text-red-700",
|
"text-red-700",
|
||||||
|
"z-20",
|
||||||
"shadow-sm",
|
"shadow-sm",
|
||||||
"transition",
|
"transition",
|
||||||
"group-hover:flex",
|
"group-hover:flex",
|
||||||
"hover:text-red-900",
|
"hover:text-red-900",
|
||||||
"hover:border-red-300",
|
"hover:border-red-300",
|
||||||
].join(" ");
|
].join(" ");
|
||||||
deleteButton.innerHTML = '<i class="ri-delete-bin-line"></i>';
|
if (isPendingDelete) {
|
||||||
|
deleteButton.classList.remove("border-red-200", "text-red-700");
|
||||||
|
deleteButton.classList.add("border-amber-300", "bg-amber-100", "text-amber-900", "hover:border-amber-400", "hover:text-amber-950");
|
||||||
|
deleteButton.innerHTML = '<i class="ri-arrow-go-back-line mr-1"></i>Rueckgaengig';
|
||||||
|
} else {
|
||||||
|
deleteButton.innerHTML = '<i class="ri-delete-bin-line mr-1"></i>Entfernen';
|
||||||
|
}
|
||||||
deleteButton.addEventListener("click", (event) => {
|
deleteButton.addEventListener("click", (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
this._openDeleteDialog({
|
this._togglePendingDelete(image.name);
|
||||||
endpoint: deleteEndpoint,
|
|
||||||
contentId,
|
|
||||||
csrfToken,
|
|
||||||
fileName: image.name,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
wrapper.appendChild(deleteButton);
|
wrapper.appendChild(deleteButton);
|
||||||
}
|
}
|
||||||
@@ -217,9 +203,49 @@ export class ContentImages extends HTMLElement {
|
|||||||
list.appendChild(wrapper);
|
list.appendChild(wrapper);
|
||||||
});
|
});
|
||||||
|
|
||||||
const uploadTile = this._findUploadTile();
|
this._pendingUrls.forEach((url, index) => {
|
||||||
if (uploadTile) {
|
const wrapper = document.createElement("div");
|
||||||
list.appendChild(uploadTile);
|
wrapper.className = "group relative";
|
||||||
|
wrapper.dataset.role = "content-images-pending";
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.type = "button";
|
||||||
|
button.className = [
|
||||||
|
"rounded",
|
||||||
|
"border",
|
||||||
|
"border-dashed",
|
||||||
|
"border-slate-300",
|
||||||
|
"bg-stone-50",
|
||||||
|
"p-1",
|
||||||
|
"shadow-sm",
|
||||||
|
].join(" ");
|
||||||
|
button.dataset.imageUrl = url;
|
||||||
|
button.dataset.imageIndex = `pending-${index}`;
|
||||||
|
const img = document.createElement("img");
|
||||||
|
img.src = url;
|
||||||
|
img.alt = "Digitalisat (neu)";
|
||||||
|
img.loading = "lazy";
|
||||||
|
img.className = "h-28 w-28 object-cover opacity-70";
|
||||||
|
button.appendChild(img);
|
||||||
|
const badge = document.createElement("span");
|
||||||
|
badge.className = "absolute left-1 top-1 rounded bg-amber-200 px-1.5 py-0.5 text-[10px] font-semibold text-amber-900";
|
||||||
|
badge.textContent = "Neu";
|
||||||
|
wrapper.appendChild(button);
|
||||||
|
wrapper.appendChild(badge);
|
||||||
|
const removeButton = document.createElement("button");
|
||||||
|
removeButton.type = "button";
|
||||||
|
removeButton.className = "absolute right-1 top-1 hidden rounded-full border border-red-200 bg-white/90 px-2 py-1 text-xs font-semibold text-red-700 shadow-sm transition group-hover:flex hover:text-red-900 hover:border-red-300";
|
||||||
|
removeButton.innerHTML = '<i class="ri-close-line mr-1"></i>Entfernen';
|
||||||
|
removeButton.addEventListener("click", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
this._removePendingFile(index);
|
||||||
|
});
|
||||||
|
wrapper.appendChild(removeButton);
|
||||||
|
list.appendChild(wrapper);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (uploadProxy && uploadProxy.parentElement !== list) {
|
||||||
|
list.appendChild(uploadProxy);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dialog = this._ensureDialog();
|
const dialog = this._ensureDialog();
|
||||||
@@ -231,7 +257,8 @@ export class ContentImages extends HTMLElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const url = target.dataset.imageUrl || "";
|
const url = target.dataset.imageUrl || "";
|
||||||
fullImage.src = buildFullUrl(url);
|
const fullUrl = url.startsWith("blob:") ? url : buildFullUrl(url);
|
||||||
|
fullImage.src = fullUrl;
|
||||||
fullImage.alt = "Digitalisat";
|
fullImage.alt = "Digitalisat";
|
||||||
if (dialog.showModal) {
|
if (dialog.showModal) {
|
||||||
dialog.showModal();
|
dialog.showModal();
|
||||||
@@ -266,6 +293,74 @@ export class ContentImages extends HTMLElement {
|
|||||||
return upload;
|
return upload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ensureUploadProxy() {
|
||||||
|
const panel = this.closest("[data-role='content-images-panel']");
|
||||||
|
if (!panel) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const uploadInput = panel.querySelector("[data-role='content-images-upload-input']");
|
||||||
|
if (!uploadInput) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let proxy = panel.querySelector("[data-role='content-images-upload-proxy']");
|
||||||
|
if (!proxy) {
|
||||||
|
proxy = document.createElement("button");
|
||||||
|
proxy.type = "button";
|
||||||
|
proxy.dataset.role = "content-images-upload-proxy";
|
||||||
|
proxy.className = "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";
|
||||||
|
proxy.setAttribute("aria-label", "Bilder hinzufuegen");
|
||||||
|
proxy.innerHTML = '<i class="ri-upload-2-line"></i>';
|
||||||
|
proxy.addEventListener("click", () => {
|
||||||
|
uploadInput.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
_togglePendingDelete(fileName) {
|
||||||
|
if (!fileName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this._pendingDeletes.has(fileName)) {
|
||||||
|
this._pendingDeletes.delete(fileName);
|
||||||
|
} else {
|
||||||
|
this._pendingDeletes.add(fileName);
|
||||||
|
}
|
||||||
|
this._render(this._currentImages || []);
|
||||||
|
}
|
||||||
|
|
||||||
|
_removePendingFile(index) {
|
||||||
|
if (index < 0 || index >= this._pendingFiles.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = this._pendingUrls[index];
|
||||||
|
if (url) {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
this._pendingFiles.splice(index, 1);
|
||||||
|
this._pendingUrls.splice(index, 1);
|
||||||
|
this._render(this._currentImages || []);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPendingFiles() {
|
||||||
|
return Array.isArray(this._pendingFiles) ? this._pendingFiles : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
getPendingDeletes() {
|
||||||
|
return Array.from(this._pendingDeletes || []);
|
||||||
|
}
|
||||||
|
|
||||||
|
_clearPendingPreviews() {
|
||||||
|
if (Array.isArray(this._pendingUrls)) {
|
||||||
|
this._pendingUrls.forEach((url) => URL.revokeObjectURL(url));
|
||||||
|
}
|
||||||
|
this._pendingUrls = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
this._clearPendingPreviews();
|
||||||
|
}
|
||||||
|
|
||||||
_ensureDialog() {
|
_ensureDialog() {
|
||||||
let dialog = this.querySelector(`[data-role='${CONTENT_IMAGES_DIALOG_ROLE}']`);
|
let dialog = this.querySelector(`[data-role='${CONTENT_IMAGES_DIALOG_ROLE}']`);
|
||||||
if (dialog) {
|
if (dialog) {
|
||||||
|
|||||||
@@ -82,6 +82,24 @@
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content-image-pending .content-image-pending-button::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: repeating-linear-gradient(-45deg, transparent, transparent 6px, rgba(220, 38, 38, 0.35) 6px, rgba(220, 38, 38, 0.35) 10px);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-image-pending .content-image-pending-button > * {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@apply normal-nums;
|
@apply normal-nums;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user