// Progressive enhancement for the upload form. With JavaScript off the plain // multipart POST still works; everything here is additive. (function () { 'use strict'; // Copy buttons appear on the server-rendered result page too, which has no // upload form, so they are wired before anything form-specific bails out. // They ship hidden so that without this script there is no dead button. function wireCopy() { var buttons = document.querySelectorAll('button.copy[data-copy]'); Array.prototype.forEach.call(buttons, function (button) { var target = document.getElementById(button.dataset.copy); if (!target) return; button.hidden = false; button.addEventListener('click', function () { target.select(); var done = function () { var was = button.textContent; button.textContent = 'Copied'; setTimeout(function () { button.textContent = was; }, 1500); }; if (navigator.clipboard) navigator.clipboard.writeText(target.value).then(done, function () {}); else { document.execCommand('copy'); done(); } }); }); } wireCopy(); // Destructive buttons ask first. The attribute carries the question, so the // markup decides what is worth confirming and this stays generic. Array.prototype.forEach.call(document.querySelectorAll('[data-confirm]'), function (button) { button.addEventListener('click', function (e) { if (!window.confirm(button.dataset.confirm)) e.preventDefault(); }); }); var form = document.getElementById('upload'); if (!form) return; var fileInput = document.getElementById('file'); var drop = document.getElementById('drop'); var dropHint = document.getElementById('drop-hint'); var progress = document.getElementById('progress'); var barFill = document.getElementById('bar-fill'); var progressText = document.getElementById('progress-text'); var submit = document.getElementById('submit'); var errorBox = document.getElementById('error'); // The session's limits are rendered by the server, so this script never has // to ask who the visitor is. Zero means unlimited. var maxSize = parseInt(form.dataset.maxSize, 10) || 0; function formatSize(n) { var units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'], i = 0; while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; } return (i === 0 ? n : n.toFixed(2).replace(/\.00$/, '')) + ' ' + units[i]; } function showError(msg) { errorBox.textContent = msg; errorBox.hidden = !msg; } // --- drag and drop ------------------------------------------------------ function describeSelection() { var f = fileInput.files[0]; dropHint.textContent = f ? f.name + ' - ' + formatSize(f.size) : 'Choose a file, or drop one here.'; } ['dragenter', 'dragover'].forEach(function (name) { drop.addEventListener(name, function (e) { e.preventDefault(); drop.classList.add('over'); }); }); ['dragleave', 'drop'].forEach(function (name) { drop.addEventListener(name, function () { drop.classList.remove('over'); }); }); drop.addEventListener('drop', function (e) { e.preventDefault(); if (!e.dataTransfer.files.length) return; // DataTransfer is the only way to put a dropped file into a file input. var dt = new DataTransfer(); dt.items.add(e.dataTransfer.files[0]); fileInput.files = dt.files; describeSelection(); }); fileInput.addEventListener('change', describeSelection); // --- upload ------------------------------------------------------------- form.addEventListener('submit', function (e) { var file = fileInput.files[0]; if (!file) return; // let the browser's own validation speak if (maxSize && file.size > maxSize) { e.preventDefault(); showError('That file is ' + formatSize(file.size) + '; your limit is ' + formatSize(maxSize) + '.'); return; } e.preventDefault(); showError(''); submit.disabled = true; progress.hidden = false; // FormData follows markup order, not painted order, so the options and the // token precede the file part exactly as the server requires. The drop // zone only looks like it comes first; see the order rules in style.css. var data = new FormData(form); var started = Date.now(); var xhr = new XMLHttpRequest(); xhr.open('POST', form.action); // Ask for the page a plain form post would have been given. What a // finished upload looks like is the server's business, and it already // renders it; this script only adds the progress bar on the way there. xhr.setRequestHeader('Accept', 'text/html'); xhr.upload.onprogress = function (ev) { if (!ev.lengthComputable) return; var pct = ev.loaded / ev.total; barFill.style.width = (pct * 100).toFixed(1) + '%'; var elapsed = (Date.now() - started) / 1000; var rate = elapsed > 0 ? ev.loaded / elapsed : 0; var eta = rate > 0 ? (ev.total - ev.loaded) / rate : 0; progressText.textContent = (pct * 100).toFixed(0) + '% - ' + formatSize(ev.loaded) + ' of ' + formatSize(ev.total) + ' - ' + formatSize(rate) + '/s' + (eta > 1 ? ', ' + formatTime(eta) + ' left' : ''); }; xhr.onload = function () { // Whatever came back, the result page or an error page, is the page the // browser should be showing now. Writing it into this document rather // than navigating leaves the address on the form's own URL, so a reload // offers a fresh form instead of posting the file a second time. document.open(); document.write(xhr.responseText); document.close(); }; xhr.onerror = function () { submit.disabled = false; progress.hidden = true; showError('Upload failed: the connection was lost.'); }; xhr.send(data); }); function formatTime(s) { s = Math.round(s); if (s < 60) return s + 's'; var m = Math.floor(s / 60); if (m < 60) return m + 'm ' + (s % 60) + 's'; return Math.floor(m / 60) + 'h ' + (m % 60) + 'm'; } })();