copy paste images in admin

This commit is contained in:
Harlekin committed 2026-10-25 22:07:13 +01:00
1 parent bef622b61f
commit 42ca51d58b
9 files changed
+189 -13

No files matched your search

+90
View File
@@ -29,8 +29,98 @@ function rowIndexes(field) {
return indexes;
}
// Images pasted or dropped into a text field are uploaded right away and inserted as Markdown.
// The form says where they go (data-image-upload), the text field opts in with data-images.
let uploading = 0;
let placeholders = 0;
async function uploadImage(form, file, name) {
const body = new FormData();
body.set('_csrf', form.elements._csrf.value);
body.set('bild', file, name);
const res = await fetch(form.dataset.imageUpload, { method: 'POST', body });
const type = res.headers.get('content-type') ?? '';
const data = type.includes('json') ? await res.json() : null;
if (res.ok && data?.src) return data.src;
const message = data?.error ?? (type.startsWith('text/plain') ? await res.text() : '');
throw new Error(message || 'Das Bild konnte nicht hochgeladen werden.');
}
function replaceText(area, search, replacement) {
const start = area.value.indexOf(search);
if (start !== -1) area.setRangeText(replacement, start, start + search.length, 'preserve');
}
async function insertImages(area, files, pasted) {
if (!area.form.dataset.imageUpload) {
window.alert('Speichere das Projekt einmal, danach kannst du Bilder in den Text einfügen.');
return;
}
// Each image gets a paragraph of its own, glued to the text around it would break headings
const before = area.value.slice(0, area.selectionStart);
const after = area.value.slice(area.selectionEnd);
const lead = !before || before.endsWith('\n\n') ? '' : before.endsWith('\n') ? '\n' : '\n\n';
const trail = !after || after.startsWith('\n\n') ? '' : after.startsWith('\n') ? '\n' : '\n\n';
const marks = files.map(() => `![Bild ${++placeholders} wird hochgeladen …]()`);
area.setRangeText(lead + marks.join('\n\n') + trail, area.selectionStart, area.selectionEnd, 'end');
// One after the other, so two pasted screenshots don't race for the same file name
uploading++;
try {
for (const [i, file] of files.entries()) {
try {
const src = await uploadImage(area.form, file, pasted ? 'bild' : file.name);
replaceText(area, marks[i], `![](${src})`);
} catch (err) {
replaceText(area, marks[i], '');
window.alert(err.message);
}
}
} finally {
uploading--;
}
}
const imagesIn = (list) => [...(list?.files ?? [])].filter((file) => file.type.startsWith('image/'));
const closest = (event, selector) => (event.target instanceof Element ? event.target.closest(selector) : null);
const hasFiles = (event) => [...(event.dataTransfer?.types ?? [])].includes('Files');
document.addEventListener('paste', (event) => {
const area = closest(event, 'textarea[data-images]');
const files = imagesIn(event.clipboardData);
// Office programs put a picture of copied text on the clipboard too, then the text wins
if (!area || !files.length || event.clipboardData.getData('text/plain').trim()) return;
event.preventDefault();
insertImages(area, files, true);
});
// Without this a file dropped next to a text field opens in the tab and the form is gone
document.addEventListener('dragover', (event) => {
if (!hasFiles(event) || closest(event, 'input[type="file"]')) return;
event.preventDefault();
event.dataTransfer.dropEffect = closest(event, 'textarea[data-images]') ? 'copy' : 'none';
});
document.addEventListener('drop', (event) => {
if (!hasFiles(event) || closest(event, 'input[type="file"]')) return;
event.preventDefault();
const area = closest(event, 'textarea[data-images]');
const files = imagesIn(event.dataTransfer);
if (!area || !files.length) return;
// The cursor doesn't follow a dropped file, so without one placed in the field the image goes at the end
if (document.activeElement !== area) area.setSelectionRange(area.value.length, area.value.length);
area.focus();
insertImages(area, files, false);
});
document.addEventListener('submit', (event) => {
const form = event.target;
if (uploading) {
event.preventDefault();
window.alert('Einen Moment noch, die Bilder werden noch hochgeladen.');
return;
}
if (form.dataset.confirm && !window.confirm(form.dataset.confirm)) {
event.preventDefault();
return;