Upload via GUI (396 Dateien)

This commit is contained in:
2026-11-04 07:56:07 +00:00
parent 3a408cd8dc
commit 79c6424c77
8 changed files with 524 additions and 8 deletions
+27
View File
@@ -198,6 +198,33 @@
imgBtn.addEventListener('click', () => fileInput.click());
toolbar.appendChild(imgBtn);
toolbar.appendChild(fileInput);
// Bild aus der Zwischenablage (Strg + V, zum Beispiel ein Screenshot) oder per Ziehen in das Feld: derselbe Weg wie der Knopf.
// Enthält die Zwischenablage auch Text (Kopie aus Word, Webseite), bleibt das normale Einfügen unverändert.
const imageFrom = (list) => Array.from(list || []).find((f) => /^image\/(png|jpe?g|gif)$/i.test(f.type));
const uploadPicked = (file) => {
const ext = file.type.toLowerCase() === 'image/jpeg' ? 'jpg' : file.type.split('/')[1].toLowerCase();
const named = /\.(png|jpe?g|gif)$/i.test(file.name || '') ? file : new File([file], 'screenshot-' + Date.now() + '.' + ext, { type: file.type });
const dt = new DataTransfer();
dt.items.add(named);
fileInput.files = dt.files;
fileInput.dispatchEvent(new Event('change'));
};
editable.addEventListener('paste', (e) => {
const cd = e.clipboardData;
if (!cd || Array.from(cd.types || []).some((t) => t === 'text/plain' || t === 'text/html')) return;
const img = imageFrom(cd.files);
if (!img) return;
e.preventDefault();
uploadPicked(img);
});
editable.addEventListener('dragover', (e) => { if (Array.from((e.dataTransfer && e.dataTransfer.types) || []).includes('Files')) e.preventDefault(); });
editable.addEventListener('drop', (e) => {
const img = imageFrom(e.dataTransfer && e.dataTransfer.files);
if (!img) return;
e.preventDefault();
uploadPicked(img);
});
}
editable.addEventListener('input', () => { textarea.value = editable.innerHTML; });
@@ -0,0 +1,204 @@
/**
* Tastenkürzel im Mitarbeiterbereich.
*
* Ticketliste: j / k = Ticket wählen, Enter oder o = öffnen, r = öffnen und gleich antworten, x = Auswahlfeld, / = Suche, n = neues Ticket
* Ticket: j / k = nächste / vorherige Nachricht, r = Antwort schreiben, u = zurück zur Liste, / = Suche in der Liste
* Überall: ? = Übersicht, Esc = schließen. Strg + Enter im Antwortfeld sendet die Antwort.
*
* Die Kürzel gelten nur, wenn kein Eingabefeld aktiv ist und keine Strg-, Alt- oder Meta-Taste gedrückt wird. Wer sie nicht möchte,
* schaltet sie in der Übersicht (?) ab; die Wahl bleibt im Browser (localStorage).
*/
(function () {
'use strict';
if (window.__deskhubShortcuts) return;
window.__deskhubShortcuts = true;
var EN = /^en/i.test(document.documentElement.lang || '');
// Die Seite wird serverseitig übersetzt, Texte aus diesem Skript nicht: deshalb hier die englische Fassung
var TXT = EN ? {
title: 'Keyboard shortcuts', nextMsg: 'Next / previous message', nextTicket: 'Next / previous ticket', open: 'Open the selected ticket (list)',
reply: 'Reply (in the list: open and select the reply field)', tick: 'Tick the selected ticket (list)', search: 'Search', create: 'New ticket',
back: 'Back to the ticket list (in a ticket)', send: 'Send the reply (in the reply field)', help: 'This overview', use: 'Use keyboard shortcuts (this browser only)',
enter: 'Enter or o', ctrl: 'Ctrl + Enter'
} : {
title: 'Tastenkürzel', nextMsg: 'Nächste / vorherige Nachricht', nextTicket: 'Nächstes / vorheriges Ticket', open: 'Gewähltes Ticket öffnen (Liste)',
reply: 'Antworten (in der Liste: öffnen und Antwortfeld wählen)', tick: 'Gewähltes Ticket ankreuzen (Liste)', search: 'Suche', create: 'Neues Ticket',
back: 'Zurück zur Ticketliste (im Ticket)', send: 'Antwort senden (im Antwortfeld)', help: 'Diese Übersicht', use: 'Tastenkürzel verwenden (nur in diesem Browser)',
enter: 'Enter oder o', ctrl: 'Strg + Enter'
};
var KEY = 'deskhub_shortcuts_off';
var path = location.pathname;
var onList = /\/staff\/tickets\.php$/.test(path);
var onTicket = /\/staff\/ticket_view\.php$/.test(path);
var cursor = -1;
var help = null;
function disabled() { try { return localStorage.getItem(KEY) === '1'; } catch (e) { return false; } }
function setDisabled(v) { try { if (v) localStorage.setItem(KEY, '1'); else localStorage.removeItem(KEY); } catch (e) { /* ohne Speicher */ } }
function typing(el) {
if (!el) return false;
var tag = (el.tagName || '').toLowerCase();
return tag === 'input' || tag === 'textarea' || tag === 'select' || el.isContentEditable === true;
}
function rows() { return Array.prototype.slice.call(document.querySelectorAll('table tbody tr')).filter(function (tr) { return tr.querySelector('a.ticket-no'); }); }
function messages() { return Array.prototype.slice.call(document.querySelectorAll('.thread-msg')); }
function mark(list, i) {
list.forEach(function (el) { el.classList.remove('kb-active'); });
if (i < 0 || i >= list.length) return;
var el = list[i];
el.classList.add('kb-active');
el.scrollIntoView({ block: 'center', behavior: 'smooth' });
}
function move(list, delta) {
if (!list.length) return;
var current = list.findIndex(function (el) { return el.classList.contains('kb-active'); });
cursor = current < 0 ? (delta > 0 ? 0 : list.length - 1) : Math.max(0, Math.min(list.length - 1, current + delta));
mark(list, cursor);
}
function activeRow() { return rows().filter(function (tr) { return tr.classList.contains('kb-active'); })[0] || null; }
function focusReply() {
var field = document.getElementById('message-field');
if (!field) return false;
var wrap = field.parentNode;
var editable = wrap && wrap.querySelector('[contenteditable]');
var target = editable || field;
target.scrollIntoView({ block: 'center', behavior: 'smooth' });
target.focus();
return true;
}
function closeHelp() { if (help) { help.remove(); help = null; } }
function showHelp() {
if (help) return closeHelp();
var items = [
['j / k', onTicket ? TXT.nextMsg : TXT.nextTicket],
[TXT.enter, TXT.open],
['r', TXT.reply],
['x', TXT.tick],
['/', TXT.search],
['n', TXT.create],
['u', TXT.back],
[TXT.ctrl, TXT.send],
['?', TXT.help],
];
help = document.createElement('div');
help.setAttribute('role', 'dialog');
help.setAttribute('aria-label', TXT.title);
help.style.cssText = 'position:fixed;inset:0;z-index:2147483600;background:rgba(15,23,42,.55);display:flex;align-items:center;justify-content:center;padding:16px';
var box = document.createElement('div');
box.style.cssText = 'background:var(--surface,#fff);color:var(--ink,#0f172a);border-radius:12px;padding:20px 24px;max-width:460px;width:100%;box-shadow:0 20px 50px rgba(0,0,0,.35);font:14px/1.5 system-ui,sans-serif';
var h = document.createElement('div');
h.style.cssText = 'font-weight:700;font-size:1.05rem;margin-bottom:10px';
h.textContent = TXT.title;
box.appendChild(h);
items.forEach(function (it) {
var row = document.createElement('div');
row.style.cssText = 'display:flex;gap:12px;padding:4px 0;border-top:1px solid rgba(148,163,184,.25)';
var k = document.createElement('kbd');
k.style.cssText = 'min-width:104px;font:600 12px ui-monospace,monospace;background:rgba(148,163,184,.2);border-radius:5px;padding:2px 7px;align-self:flex-start';
k.textContent = it[0];
var d = document.createElement('span');
d.textContent = it[1];
row.appendChild(k);
row.appendChild(d);
box.appendChild(row);
});
var label = document.createElement('label');
label.style.cssText = 'display:flex;align-items:center;gap:8px;margin-top:14px;font-weight:400';
var cb = document.createElement('input');
cb.type = 'checkbox';
cb.style.width = 'auto';
cb.checked = !disabled();
cb.addEventListener('change', function () { setDisabled(!cb.checked); });
label.appendChild(cb);
label.appendChild(document.createTextNode(TXT.use));
box.appendChild(label);
help.appendChild(box);
help.addEventListener('click', function (e) { if (e.target === help) closeHelp(); });
document.body.appendChild(help);
}
// Auffälliger Rahmen für die gewählte Zeile bzw. Nachricht
var style = document.createElement('style');
style.textContent = '.kb-active{outline:2px solid var(--primary,#0f6f6a);outline-offset:-2px;background:rgba(15,111,106,.07)}';
document.head.appendChild(style);
document.addEventListener('keydown', function (e) {
// Strg + Enter im Antwortfeld sendet die öffentliche Antwort (unabhängig von den Einzeltasten)
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey) && onTicket && !disabled()) {
var editable = e.target && e.target.closest ? e.target.closest('.richtext-editable') : null;
if (editable) {
var form = editable.closest('form');
var action = form && form.querySelector('#form-action');
if (form && action && action.value === 'reply') {
e.preventDefault();
var ta = form.querySelector('textarea[data-richtext]');
if (ta) ta.value = editable.innerHTML;
if (form.requestSubmit) form.requestSubmit(); else form.submit();
}
}
return;
}
if (e.key === 'Escape') { if (help) { closeHelp(); e.preventDefault(); } else if (typing(e.target)) e.target.blur(); return; }
if (e.ctrlKey || e.altKey || e.metaKey) return;
if (typing(e.target)) return;
if (e.key === '?') { e.preventDefault(); showHelp(); return; }
if (disabled() || help) return;
switch (e.key) {
case 'j': case 'k':
if (onList) { e.preventDefault(); move(rows(), e.key === 'j' ? 1 : -1); }
else if (onTicket) { e.preventDefault(); move(messages(), e.key === 'j' ? 1 : -1); }
break;
case 'Enter': case 'o': {
var row = onList ? activeRow() : null;
var link = row && row.querySelector('a.ticket-no');
if (link) { e.preventDefault(); location.href = link.href; }
break;
}
case 'x': {
var r = onList ? activeRow() : null;
var box = r && r.querySelector('input.ticket-check');
if (box) { e.preventDefault(); box.checked = !box.checked; box.dispatchEvent(new Event('change', { bubbles: true })); }
break;
}
case 'r':
if (onTicket) { if (focusReply()) e.preventDefault(); }
else if (onList) {
var rr = activeRow();
var a = rr && rr.querySelector('a.ticket-no');
if (a) { e.preventDefault(); location.href = a.href + '#reply'; }
}
break;
case '/': {
var search = document.querySelector('input[name="q"]');
if (search) { e.preventDefault(); search.focus(); search.select(); }
else { e.preventDefault(); location.href = '/staff/tickets.php#search'; }
break;
}
case 'n':
e.preventDefault();
location.href = '/staff/ticket_create.php';
break;
case 'u':
if (onTicket) { e.preventDefault(); location.href = '/staff/tickets.php'; }
break;
}
});
document.addEventListener('DOMContentLoaded', function () {
if (disabled()) return;
if (location.hash === '#reply' && onTicket) setTimeout(focusReply, 150);
if (location.hash === '#search' && onList) {
var s = document.querySelector('input[name="q"]');
if (s) { s.focus(); s.select(); }
}
});
})();
@@ -0,0 +1,98 @@
/**
* Bildvorschau für Anhänge im Ticket: Ein Klick auf ein Vorschaubild zeigt das Bild groß über der Seite, ohne es herunterzuladen.
* Pfeil links/rechts (oder die Knöpfe) blättern durch alle Bilder des Tickets, Esc oder ein Klick daneben schließt. Ohne JavaScript
* öffnet der Link das Bild wie bisher in einem neuen Tab.
*/
(function () {
'use strict';
var links = Array.prototype.slice.call(document.querySelectorAll('a[data-lightbox]'));
if (!links.length) return;
var EN = /^en/i.test(document.documentElement.lang || '');
var TXT = EN ? { dialog: 'Image preview', prev: 'Previous image (←)', next: 'Next image (→)', download: '⬇ Download', close: '✕ Close', closeTitle: 'Close (Esc)' }
: { dialog: 'Bildvorschau', prev: 'Vorheriges Bild (←)', next: 'Nächstes Bild (→)', download: '⬇ Herunterladen', close: '✕ Schließen', closeTitle: 'Schließen (Esc)' };
var overlay = null, img = null, caption = null, counter = null, index = 0;
function build() {
overlay = document.createElement('div');
overlay.setAttribute('role', 'dialog');
overlay.setAttribute('aria-modal', 'true');
overlay.setAttribute('aria-label', TXT.dialog);
overlay.style.cssText = 'position:fixed;inset:0;z-index:2147483500;background:rgba(2,6,23,.88);display:none;flex-direction:column;align-items:center;justify-content:center;padding:16px;gap:10px';
img = document.createElement('img');
img.alt = '';
img.style.cssText = 'max-width:96vw;max-height:80vh;object-fit:contain;border-radius:6px;background:#fff;box-shadow:0 10px 40px rgba(0,0,0,.5)';
caption = document.createElement('div');
caption.style.cssText = 'color:#e2e8f0;font:14px system-ui,sans-serif;text-align:center;max-width:90vw;word-break:break-all';
counter = document.createElement('span');
counter.style.cssText = 'color:#94a3b8;margin-left:8px';
var bar = document.createElement('div');
bar.style.cssText = 'display:flex;gap:8px;flex-wrap:wrap;justify-content:center';
function btn(label, title, fn) {
var b = document.createElement('button');
b.type = 'button';
b.textContent = label;
b.title = title;
b.style.cssText = 'background:#1e293b;color:#fff;border:1px solid #475569;border-radius:8px;padding:7px 14px;cursor:pointer;font:600 14px system-ui,sans-serif';
b.addEventListener('click', function (e) { e.stopPropagation(); fn(); });
return b;
}
var download = document.createElement('a');
download.textContent = TXT.download;
download.style.cssText = 'background:#1e293b;color:#fff;border:1px solid #475569;border-radius:8px;padding:7px 14px;text-decoration:none;font:600 14px system-ui,sans-serif';
download.addEventListener('click', function (e) { e.stopPropagation(); });
overlay._download = download;
bar.appendChild(btn('‹', TXT.prev, function () { show(index - 1); }));
bar.appendChild(btn('›', TXT.next, function () { show(index + 1); }));
bar.appendChild(download);
bar.appendChild(btn(TXT.close, TXT.closeTitle, close));
caption.appendChild(document.createTextNode(''));
overlay.appendChild(img);
overlay.appendChild(caption);
overlay.appendChild(bar);
overlay.addEventListener('click', close);
document.body.appendChild(overlay);
}
function show(i) {
if (!links.length) return;
index = (i + links.length) % links.length;
var a = links[index];
img.src = a.getAttribute('href');
img.alt = a.getAttribute('title') || '';
caption.textContent = a.getAttribute('title') || '';
counter.textContent = links.length > 1 ? '(' + (index + 1) + ' / ' + links.length + ')' : '';
caption.appendChild(counter);
overlay._download.href = a.getAttribute('data-download') || a.getAttribute('href');
overlay._download.setAttribute('download', a.getAttribute('title') || '');
}
function open(i) {
if (!overlay) build();
overlay.style.display = 'flex';
document.body.style.overflow = 'hidden';
show(i);
}
function close() {
if (!overlay) return;
overlay.style.display = 'none';
img.removeAttribute('src');
document.body.style.overflow = '';
}
links.forEach(function (a, i) {
a.addEventListener('click', function (e) {
if (e.ctrlKey || e.metaKey || e.shiftKey || e.button === 1) return; // neuer Tab bleibt möglich
e.preventDefault();
open(i);
});
});
document.addEventListener('keydown', function (e) {
if (!overlay || overlay.style.display === 'none') return;
if (e.key === 'Escape') { e.preventDefault(); close(); }
else if (e.key === 'ArrowLeft') { e.preventDefault(); show(index - 1); }
else if (e.key === 'ArrowRight') { e.preventDefault(); show(index + 1); }
});
})();