// Everything here is additive: with JavaScript off, the plain multipart POST // still works. (function () { 'use strict'; // Wired before the form-specific code bails out, because the result page has // copy buttons and no form. They ship hidden, so without this 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; var label = button.textContent, timer; function say(word) { button.textContent = word; clearTimeout(timer); timer = setTimeout(function () { button.textContent = label; }, 1500); } // Answers both ways the clipboard API fails us: absent over plain HTTP, // where it is not a secure context, and present but refusing. Needs no // permission. If it fails too, the text is selected, so say what to press. function copySelection() { var ok = false; try { ok = document.execCommand('copy'); } catch (e) { /* ok stays false */ } say(ok ? 'Copied' : 'Press Ctrl-C'); } button.addEventListener('click', function () { target.select(); target.setSelectionRange(0, target.value.length); // iOS ignores select() if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(target.value).then(function () { say('Copied'); }, copySelection); } else { copySelection(); } }); }); } wireCopy(); 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'); // 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; } 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); 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 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 get: the server owns what a // finished upload looks like, and this only adds a progress bar. 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 () { // Writing the response into this document rather than navigating leaves // the address on the form's URL, so a reload offers a fresh form instead // of posting the file again. 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'; } })();