forgejo-tickets/web/static/js/attachments.js

180 lines
5.4 KiB
JavaScript

/**
* initAttachmentZone — wire up drag-and-drop, clipboard paste, and file-picker
* for a form that already has a multipart file input.
*
* Uses a DataTransfer object as a mutable file store so that
* fileInput.files always reflects the full set of chosen files and
* native form submission works unchanged.
*/
function initAttachmentZone({ formEl, fileInput, dropZone, fileListEl, pasteTarget }) {
const dt = new DataTransfer();
let dragCounter = 0;
// ── Helpers ──────────────────────────────────────────────────────────
function fileKey(f) {
return f.name + '|' + f.size + '|' + f.lastModified;
}
function currentKeys() {
const keys = new Set();
for (let i = 0; i < dt.items.length; i++) {
keys.add(fileKey(dt.files[i]));
}
return keys;
}
function addFiles(files) {
const keys = currentKeys();
for (const f of files) {
if (!keys.has(fileKey(f))) {
dt.items.add(f);
keys.add(fileKey(f));
}
}
sync();
}
function removeFile(index) {
dt.items.remove(index);
sync();
}
function sync() {
fileInput.files = dt.files;
renderList();
}
// ── File list rendering ─────────────────────────────────────────────
function formatSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
function renderList() {
fileListEl.innerHTML = '';
if (dt.files.length === 0) {
fileListEl.classList.add('hidden');
return;
}
fileListEl.classList.remove('hidden');
for (let i = 0; i < dt.files.length; i++) {
const f = dt.files[i];
const row = document.createElement('div');
row.className = 'flex items-center justify-between rounded-md bg-gray-50 px-3 py-1.5 text-sm';
const info = document.createElement('span');
info.className = 'text-gray-700 truncate';
info.textContent = f.name;
const size = document.createElement('span');
size.className = 'text-gray-400 ml-2 whitespace-nowrap';
size.textContent = formatSize(f.size);
const left = document.createElement('div');
left.className = 'flex items-center min-w-0';
left.appendChild(info);
left.appendChild(size);
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'ml-2 text-gray-400 hover:text-red-500 text-xs font-bold';
btn.textContent = '\u00d7';
btn.setAttribute('aria-label', 'Remove ' + f.name);
btn.addEventListener('click', function () { removeFile(i); });
row.appendChild(left);
row.appendChild(btn);
fileListEl.appendChild(row);
}
}
// ── Drag-and-drop ───────────────────────────────────────────────────
function highlight() {
dropZone.classList.add('ring-blue-400', 'bg-blue-50', 'border-blue-400');
}
function unhighlight() {
dropZone.classList.remove('ring-blue-400', 'bg-blue-50', 'border-blue-400');
}
dropZone.addEventListener('dragenter', function (e) {
e.preventDefault();
dragCounter++;
highlight();
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dragCounter--;
if (dragCounter <= 0) {
dragCounter = 0;
unhighlight();
}
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dragCounter = 0;
unhighlight();
if (e.dataTransfer && e.dataTransfer.files.length) {
addFiles(e.dataTransfer.files);
}
});
// ── File picker (additive) ──────────────────────────────────────────
fileInput.addEventListener('change', function () {
addFiles(fileInput.files);
});
// ── Clipboard paste ─────────────────────────────────────────────────
function pasteTimestamp() {
const d = new Date();
const pad = function (n) { return n < 10 ? '0' + n : '' + n; };
return d.getFullYear() +
pad(d.getMonth() + 1) +
pad(d.getDate()) + '-' +
pad(d.getHours()) +
pad(d.getMinutes()) +
pad(d.getSeconds());
}
if (pasteTarget) {
pasteTarget.addEventListener('paste', function (e) {
var items = e.clipboardData && e.clipboardData.items;
if (!items) return;
var files = [];
for (var i = 0; i < items.length; i++) {
if (items[i].kind === 'file') {
var f = items[i].getAsFile();
if (!f) continue;
// Rename generic pasted names like "image.png"
if (/^image\.\w+$/.test(f.name)) {
var ext = f.name.split('.').pop();
var renamed = new File([f], 'paste-' + pasteTimestamp() + '.' + ext, { type: f.type });
files.push(renamed);
} else {
files.push(f);
}
}
}
if (files.length > 0) {
addFiles(files);
// Don't prevent default — let text pastes through
}
});
}
}