Files
uncensored-send/web/static/app.js
T

160 lines
6.0 KiB
JavaScript

// 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';
}
})();