mirror of
https://github.com/Theodor-Springmann-Stiftung/musenalm.git
synced 2026-02-04 10:35:30 +00:00
some list things; image uplaod
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -223,7 +223,6 @@
|
|||||||
{{- if $i }},{{ end }}
|
{{- if $i }},{{ end }}
|
||||||
"{{ $content.Id }}": {
|
"{{ $content.Id }}": {
|
||||||
extent: "{{ $content.Extent }}",
|
extent: "{{ $content.Extent }}",
|
||||||
pagination: "{{ $content.MusenalmPagination }}",
|
|
||||||
musenalmType: [{{- range $j, $t := $content.MusenalmType -}}{{- if $j }},{{ end }}"{{ $t }}"{{- end -}}],
|
musenalmType: [{{- range $j, $t := $content.MusenalmType -}}{{- if $j }},{{ end }}"{{ $t }}"{{- end -}}],
|
||||||
preferredTitle: "{{ $content.PreferredTitle }}",
|
preferredTitle: "{{ $content.PreferredTitle }}",
|
||||||
titleStmt: "{{ $content.TitleStmt }}",
|
titleStmt: "{{ $content.TitleStmt }}",
|
||||||
@@ -296,7 +295,6 @@
|
|||||||
// Field labels for display
|
// Field labels for display
|
||||||
const fieldLabels = {
|
const fieldLabels = {
|
||||||
extent: 'Seite',
|
extent: 'Seite',
|
||||||
pagination: 'Paginierung',
|
|
||||||
musenalmType: 'Typ',
|
musenalmType: 'Typ',
|
||||||
titleStmt: 'Titel',
|
titleStmt: 'Titel',
|
||||||
subtitleStmt: 'Untertitel',
|
subtitleStmt: 'Untertitel',
|
||||||
@@ -311,6 +309,51 @@
|
|||||||
annotation: 'Anmerkung'
|
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 performFilter = () => {
|
||||||
const query = normalizeText(filterInput.value);
|
const query = normalizeText(filterInput.value);
|
||||||
const pageQuery = normalizeText(filterPage?.value || '');
|
const pageQuery = normalizeText(filterPage?.value || '');
|
||||||
@@ -401,8 +444,6 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Check text search fields (excluding language, status, preferredTitle)
|
// 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);
|
checkArrayField('musenalmType', contentData.musenalmType, fieldLabels.musenalmType);
|
||||||
checkField('titleStmt', contentData.titleStmt, fieldLabels.titleStmt);
|
checkField('titleStmt', contentData.titleStmt, fieldLabels.titleStmt);
|
||||||
checkField('subtitleStmt', contentData.subtitleStmt, fieldLabels.subtitleStmt);
|
checkField('subtitleStmt', contentData.subtitleStmt, fieldLabels.subtitleStmt);
|
||||||
@@ -463,6 +504,8 @@
|
|||||||
? `${totalCount} Einträge`
|
? `${totalCount} Einträge`
|
||||||
: `${visibleCount} von ${totalCount} Einträgen`;
|
: `${visibleCount} von ${totalCount} Einträgen`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateMarks();
|
||||||
};
|
};
|
||||||
|
|
||||||
const debounceFilter = () => {
|
const debounceFilter = () => {
|
||||||
@@ -522,6 +565,15 @@
|
|||||||
}
|
}
|
||||||
syncIndicator.classList.toggle("hidden", !active);
|
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
|
// Shared delete dialog
|
||||||
const deleteDialog = document.getElementById("content-delete-dialog");
|
const deleteDialog = document.getElementById("content-delete-dialog");
|
||||||
const deleteDialogTitle = document.getElementById("content-delete-dialog-title");
|
const deleteDialogTitle = document.getElementById("content-delete-dialog-title");
|
||||||
@@ -665,8 +717,59 @@
|
|||||||
if (list.dataset.pageInit !== "true") {
|
if (list.dataset.pageInit !== "true") {
|
||||||
list.dataset.pageInit = "true";
|
list.dataset.pageInit = "true";
|
||||||
let draggedItem = null;
|
let draggedItem = null;
|
||||||
|
let pointerDrag = null;
|
||||||
let lastDragOverTime = 0;
|
let lastDragOverTime = 0;
|
||||||
const DRAG_THROTTLE_MS = 100;
|
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) => {
|
list.addEventListener("click", (event) => {
|
||||||
const moveUp = event.target.closest("[data-role='content-move-up']");
|
const moveUp = event.target.closest("[data-role='content-move-up']");
|
||||||
const moveDown = event.target.closest("[data-role='content-move-down']");
|
const moveDown = event.target.closest("[data-role='content-move-down']");
|
||||||
@@ -699,6 +802,10 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
list.addEventListener("dragstart", (event) => {
|
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']")) {
|
if (event.target.closest("[data-role='content-move-up']") || event.target.closest("[data-role='content-move-down']")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -717,6 +824,7 @@
|
|||||||
draggedItem = item;
|
draggedItem = item;
|
||||||
item.dataset.dragging = "true";
|
item.dataset.dragging = "true";
|
||||||
draggedItem.classList.add("opacity-60");
|
draggedItem.classList.add("opacity-60");
|
||||||
|
setDraggingState(true);
|
||||||
event.dataTransfer.effectAllowed = "move";
|
event.dataTransfer.effectAllowed = "move";
|
||||||
event.dataTransfer.setData("text/plain", "move");
|
event.dataTransfer.setData("text/plain", "move");
|
||||||
});
|
});
|
||||||
@@ -745,8 +853,14 @@
|
|||||||
draggedItem.dataset.dragging = "";
|
draggedItem.dataset.dragging = "";
|
||||||
}
|
}
|
||||||
draggedItem = null;
|
draggedItem = null;
|
||||||
|
setDraggingState(false);
|
||||||
syncOrder();
|
syncOrder();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
list.addEventListener("pointerdown", startPointerDrag);
|
||||||
|
list.addEventListener("pointermove", movePointerDrag);
|
||||||
|
list.addEventListener("pointerup", endPointerDrag);
|
||||||
|
list.addEventListener("pointercancel", endPointerDrag);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -209,6 +209,12 @@
|
|||||||
data-initial-values='[{{- range $i, $lang := $model.content.Language -}}{{- if $i }},{{ end -}}{{ printf "%q" $lang }}{{- end -}}]'>
|
data-initial-values='[{{- range $i, $lang := $model.content.Language -}}{{- if $i }},{{ end -}}{{ printf "%q" $lang }}{{- end -}}]'>
|
||||||
</multi-select-simple>
|
</multi-select-simple>
|
||||||
</div>
|
</div>
|
||||||
|
{{- template "_content_images_panel" (Dict
|
||||||
|
"content" $model.content
|
||||||
|
"entry" $model.result.Entry
|
||||||
|
"csrf_token" $model.csrf_token
|
||||||
|
"is_new" false
|
||||||
|
) -}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -4,30 +4,18 @@
|
|||||||
{{- $isNew := index . "is_new" -}}
|
{{- $isNew := index . "is_new" -}}
|
||||||
|
|
||||||
{{- if or $content.ImagePaths (not $isNew) -}}
|
{{- 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="w-full" data-role="content-images-panel">
|
||||||
<div class="flex flex-wrap items-start gap-2">
|
<div class="flex flex-col items-start gap-2">
|
||||||
{{- if $content.ImagePaths -}}
|
|
||||||
<content-images
|
<content-images
|
||||||
|
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 -}}]'
|
||||||
data-files='[{{- range $i, $scan := $content.Scans -}}{{- 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-delete-endpoint="/almanach/{{ $entry.MusenalmID }}/contents/scan/delete"
|
||||||
data-content-id="{{ $content.Id }}"
|
data-content-id="{{ $content.Id }}"
|
||||||
data-csrf-token="{{ $csrf }}">
|
data-csrf-token="{{ $csrf }}">
|
||||||
</content-images>
|
</content-images>
|
||||||
{{- end -}}
|
|
||||||
{{- if not $isNew -}}
|
{{- if not $isNew -}}
|
||||||
<form
|
<div class="flex" data-role="content-images-upload">
|
||||||
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 }}" />
|
|
||||||
<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"
|
||||||
@@ -40,8 +28,12 @@
|
|||||||
name="scans"
|
name="scans"
|
||||||
multiple
|
multiple
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
class="sr-only" />
|
class="sr-only"
|
||||||
</form>
|
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 -}}
|
{{- end -}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,21 +37,25 @@
|
|||||||
</tool-tip>
|
</tool-tip>
|
||||||
</div>
|
</div>
|
||||||
{{- if $content.Extent -}}
|
{{- 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 -}}
|
{{- end -}}
|
||||||
{{- if $content.MusenalmType -}}
|
{{- if $content.MusenalmType -}}
|
||||||
<span class="flex flex-nowrap gap-1 text-gray-700 font-normal overflow-hidden">
|
<span class="flex flex-nowrap gap-1 text-gray-700 font-normal overflow-hidden">
|
||||||
{{- range $i, $t := $content.MusenalmType -}}
|
{{- 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 -}}
|
{{- end -}}
|
||||||
</span>
|
</span>
|
||||||
{{- end -}}
|
{{- 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">
|
<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">
|
||||||
|
<a
|
||||||
|
href="/almanach/{{ $entry.MusenalmID }}/contents/{{ $content.MusenalmID }}/edit"
|
||||||
|
class="no-underline hover:text-slate-900 cursor-pointer">
|
||||||
{{- if $content.PreferredTitle -}}
|
{{- if $content.PreferredTitle -}}
|
||||||
<span class="text-lg font-normal truncate min-w-0 overflow-hidden" data-role="content-header-title-text">{{- $content.PreferredTitle -}}</span>
|
<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 -}}
|
{{- 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>
|
<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 -}}
|
{{- end -}}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 flex-nowrap whitespace-nowrap shrink-0">
|
<div class="flex items-center gap-2 flex-nowrap whitespace-nowrap shrink-0">
|
||||||
@@ -79,7 +83,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ export class ContentImages extends HTMLElement {
|
|||||||
}
|
}
|
||||||
this.dataset.init = "true";
|
this.dataset.init = "true";
|
||||||
|
|
||||||
|
this._wireUpload();
|
||||||
|
|
||||||
const raw = this.getAttribute("data-images") || "[]";
|
const raw = this.getAttribute("data-images") || "[]";
|
||||||
const rawFiles = this.getAttribute("data-files") || "[]";
|
const rawFiles = this.getAttribute("data-files") || "[]";
|
||||||
let images = [];
|
let images = [];
|
||||||
@@ -82,8 +84,62 @@ export class ContentImages extends HTMLElement {
|
|||||||
this._render(normalized);
|
this._render(normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_wireUpload() {
|
||||||
|
const panel = this.closest("[data-role='content-images-panel']");
|
||||||
|
if (!panel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const uploadInput = panel.querySelector("[data-role='content-images-upload-input']");
|
||||||
|
if (!uploadInput || uploadInput.dataset.bound === "true") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uploadInput.dataset.bound = "true";
|
||||||
|
uploadInput.addEventListener("change", () => {
|
||||||
|
this._uploadFiles(uploadInput, panel);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_uploadFiles(input, panel) {
|
||||||
|
const endpoint = input.getAttribute("data-upload-endpoint") || "";
|
||||||
|
const contentId = input.getAttribute("data-content-id") || "";
|
||||||
|
const csrfToken = input.getAttribute("data-csrf-token") || "";
|
||||||
|
const files = Array.from(input.files || []);
|
||||||
|
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.classList.add("inline-flex");
|
this.classList.add("block");
|
||||||
|
this.style.display = "block";
|
||||||
|
this.style.width = "100%";
|
||||||
const list = this._ensureList();
|
const list = this._ensureList();
|
||||||
list.innerHTML = "";
|
list.innerHTML = "";
|
||||||
|
|
||||||
@@ -161,6 +217,11 @@ export class ContentImages extends HTMLElement {
|
|||||||
list.appendChild(wrapper);
|
list.appendChild(wrapper);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const uploadTile = this._findUploadTile();
|
||||||
|
if (uploadTile) {
|
||||||
|
list.appendChild(uploadTile);
|
||||||
|
}
|
||||||
|
|
||||||
const dialog = this._ensureDialog();
|
const dialog = this._ensureDialog();
|
||||||
const fullImage = dialog.querySelector(`[data-role='${CONTENT_IMAGES_FULL_ROLE}']`);
|
const fullImage = dialog.querySelector(`[data-role='${CONTENT_IMAGES_FULL_ROLE}']`);
|
||||||
|
|
||||||
@@ -185,12 +246,26 @@ export class ContentImages extends HTMLElement {
|
|||||||
if (!list) {
|
if (!list) {
|
||||||
list = document.createElement("div");
|
list = document.createElement("div");
|
||||||
list.dataset.role = CONTENT_IMAGES_LIST_ROLE;
|
list.dataset.role = CONTENT_IMAGES_LIST_ROLE;
|
||||||
list.className = "inline-flex flex-wrap gap-2";
|
|
||||||
this.appendChild(list);
|
this.appendChild(list);
|
||||||
}
|
}
|
||||||
|
list.className = "grid gap-2";
|
||||||
|
list.style.gridTemplateColumns = "repeat(auto-fill, minmax(7rem, 1fr))";
|
||||||
|
list.style.width = "100%";
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_findUploadTile() {
|
||||||
|
const panel = this.closest("[data-role='content-images-panel']");
|
||||||
|
if (!panel) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const upload = panel.querySelector("[data-role='content-images-upload']");
|
||||||
|
if (!upload) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return upload;
|
||||||
|
}
|
||||||
|
|
||||||
_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) {
|
||||||
@@ -377,11 +452,35 @@ export class ContentImages extends HTMLElement {
|
|||||||
if (!html || !panel) {
|
if (!html || !panel) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
panel.outerHTML = html;
|
this._applyServerResponse(html, panel);
|
||||||
})
|
})
|
||||||
.catch(() => null)
|
.catch(() => null)
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
dialog.close();
|
dialog.close();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_applyServerResponse(html, panel) {
|
||||||
|
const template = document.createElement("template");
|
||||||
|
template.innerHTML = html.trim();
|
||||||
|
const oobNodes = Array.from(template.content.querySelectorAll("[hx-swap-oob]"));
|
||||||
|
oobNodes.forEach((node) => {
|
||||||
|
const swapRaw = node.getAttribute("hx-swap-oob") || "";
|
||||||
|
const [swapTypeRaw, selector] = swapRaw.split(":");
|
||||||
|
const swapType = swapTypeRaw || "outerHTML";
|
||||||
|
const target = selector ? document.querySelector(selector) : (node.id ? document.getElementById(node.id) : null);
|
||||||
|
if (target) {
|
||||||
|
if (swapType === "innerHTML") {
|
||||||
|
target.innerHTML = node.innerHTML;
|
||||||
|
} else {
|
||||||
|
target.outerHTML = node.outerHTML;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node.remove();
|
||||||
|
});
|
||||||
|
const replacement = template.content.firstElementChild;
|
||||||
|
if (replacement) {
|
||||||
|
panel.replaceWith(replacement);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,22 @@
|
|||||||
@apply bg-stone-50;
|
@apply bg-stone-50;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body[data-dragging="true"] tool-tip {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-dragging="true"] .tooltip-box {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dragging tool-tip {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dragging .tooltip-box {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@apply normal-nums;
|
@apply normal-nums;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,58 @@ export class ToolTip extends HTMLElement {
|
|||||||
return ["position", "timeout"];
|
return ["position", "timeout"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static _dragGuardInitialized = false;
|
||||||
|
|
||||||
|
static _setDragging(active) {
|
||||||
|
window.__toolTipDragging = active;
|
||||||
|
if (document.documentElement) {
|
||||||
|
document.documentElement.classList.toggle("dragging", active);
|
||||||
|
}
|
||||||
|
if (document.body) {
|
||||||
|
if (active) {
|
||||||
|
document.body.dataset.dragging = "true";
|
||||||
|
} else {
|
||||||
|
delete document.body.dataset.dragging;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (active) {
|
||||||
|
document.querySelectorAll(".tooltip-box").forEach((box) => {
|
||||||
|
box.classList.remove("opacity-100");
|
||||||
|
box.classList.add("opacity-0");
|
||||||
|
box.classList.add("hidden");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static _ensureDragGuard() {
|
||||||
|
if (ToolTip._dragGuardInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ToolTip._dragGuardInitialized = true;
|
||||||
|
const start = (event) => {
|
||||||
|
const handle = event.target?.closest?.("[data-role='content-drag-handle']");
|
||||||
|
if (handle || event.type === "dragstart") {
|
||||||
|
ToolTip._setDragging(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const end = () => {
|
||||||
|
ToolTip._setDragging(false);
|
||||||
|
};
|
||||||
|
document.addEventListener("pointerdown", start, true);
|
||||||
|
document.addEventListener("mousedown", start, true);
|
||||||
|
document.addEventListener("dragstart", start, true);
|
||||||
|
document.addEventListener("pointerup", end, true);
|
||||||
|
document.addEventListener("mouseup", end, true);
|
||||||
|
document.addEventListener("pointercancel", end, true);
|
||||||
|
document.addEventListener("dragend", end, true);
|
||||||
|
document.addEventListener("drop", end, true);
|
||||||
|
window.addEventListener("blur", end);
|
||||||
|
window.addEventListener("contentsdragging", (event) => {
|
||||||
|
const active = Boolean(event.detail?.active);
|
||||||
|
ToolTip._setDragging(active);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this._tooltipBox = null;
|
this._tooltipBox = null;
|
||||||
@@ -14,6 +66,7 @@ export class ToolTip extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
|
ToolTip._ensureDragGuard();
|
||||||
this.classList.add("relative", "block", "leading-none", "[&>*]:leading-normal");
|
this.classList.add("relative", "block", "leading-none", "[&>*]:leading-normal");
|
||||||
this._dataTipElem = this.querySelector(".data-tip");
|
this._dataTipElem = this.querySelector(".data-tip");
|
||||||
const tipContent = this._dataTipElem ? this._dataTipElem.innerHTML : "Tooltip";
|
const tipContent = this._dataTipElem ? this._dataTipElem.innerHTML : "Tooltip";
|
||||||
@@ -25,6 +78,7 @@ export class ToolTip extends HTMLElement {
|
|||||||
this._tooltipBox = document.createElement("div");
|
this._tooltipBox = document.createElement("div");
|
||||||
this._tooltipBox.innerHTML = tipContent;
|
this._tooltipBox.innerHTML = tipContent;
|
||||||
this._tooltipBox.className = [
|
this._tooltipBox.className = [
|
||||||
|
"tooltip-box",
|
||||||
"opacity-0",
|
"opacity-0",
|
||||||
"hidden",
|
"hidden",
|
||||||
"absolute",
|
"absolute",
|
||||||
@@ -78,7 +132,32 @@ export class ToolTip extends HTMLElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_forceHide() {
|
||||||
|
clearTimeout(this._hideTimeout);
|
||||||
|
clearTimeout(this._hiddenTimeout);
|
||||||
|
if (!this._tooltipBox) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._tooltipBox.classList.remove("opacity-100");
|
||||||
|
this._tooltipBox.classList.add("opacity-0");
|
||||||
|
this._tooltipBox.classList.add("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
_isDragging() {
|
||||||
|
if (window.__toolTipDragging) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (document.body?.dataset?.dragging === "true") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return Boolean(document.querySelector("[data-dragging='true']"));
|
||||||
|
}
|
||||||
|
|
||||||
_showTooltip() {
|
_showTooltip() {
|
||||||
|
if (this._isDragging()) {
|
||||||
|
this._forceHide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
clearTimeout(this._hideTimeout);
|
clearTimeout(this._hideTimeout);
|
||||||
clearTimeout(this._hiddenTimeout);
|
clearTimeout(this._hiddenTimeout);
|
||||||
this._tooltipBox.classList.remove("hidden");
|
this._tooltipBox.classList.remove("hidden");
|
||||||
|
|||||||
Reference in New Issue
Block a user