Add upload to navbar
This commit is contained in:
+16
-26
@@ -1,11 +1,11 @@
|
|||||||
// Progressive enhancement for the upload form. With JavaScript off the plain
|
// Everything here is additive: with JavaScript off, the plain multipart POST
|
||||||
// multipart POST still works; everything here is additive.
|
// still works.
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// Copy buttons appear on the server-rendered result page too, which has no
|
// Wired before the form-specific code bails out, because the result page has
|
||||||
// upload form, so they are wired before anything form-specific bails out.
|
// copy buttons and no form. They ship hidden, so without this there is no
|
||||||
// They ship hidden so that without this script there is no dead button.
|
// dead button.
|
||||||
function wireCopy() {
|
function wireCopy() {
|
||||||
var buttons = document.querySelectorAll('button.copy[data-copy]');
|
var buttons = document.querySelectorAll('button.copy[data-copy]');
|
||||||
Array.prototype.forEach.call(buttons, function (button) {
|
Array.prototype.forEach.call(buttons, function (button) {
|
||||||
@@ -20,12 +20,9 @@
|
|||||||
timer = setTimeout(function () { button.textContent = label; }, 1500);
|
timer = setTimeout(function () { button.textContent = label; }, 1500);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copying the selection needs neither a permission nor a secure context,
|
// Answers both ways the clipboard API fails us: absent over plain HTTP,
|
||||||
// so it answers for both of the ways the clipboard API can let us down:
|
// where it is not a secure context, and present but refusing. Needs no
|
||||||
// missing entirely (this service is reached over plain HTTP as well as
|
// permission. If it fails too, the text is selected, so say what to press.
|
||||||
// HTTPS, and the API exists only on the secure side) or present and
|
|
||||||
// refusing (an unfocused document, a permissions policy). If even that
|
|
||||||
// fails, the text is selected either way, so say what to press.
|
|
||||||
function copySelection() {
|
function copySelection() {
|
||||||
var ok = false;
|
var ok = false;
|
||||||
try { ok = document.execCommand('copy'); } catch (e) { /* ok stays false */ }
|
try { ok = document.execCommand('copy'); } catch (e) { /* ok stays false */ }
|
||||||
@@ -47,8 +44,6 @@
|
|||||||
}
|
}
|
||||||
wireCopy();
|
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) {
|
Array.prototype.forEach.call(document.querySelectorAll('[data-confirm]'), function (button) {
|
||||||
button.addEventListener('click', function (e) {
|
button.addEventListener('click', function (e) {
|
||||||
if (!window.confirm(button.dataset.confirm)) e.preventDefault();
|
if (!window.confirm(button.dataset.confirm)) e.preventDefault();
|
||||||
@@ -67,8 +62,7 @@
|
|||||||
var submit = document.getElementById('submit');
|
var submit = document.getElementById('submit');
|
||||||
var errorBox = document.getElementById('error');
|
var errorBox = document.getElementById('error');
|
||||||
|
|
||||||
// The session's limits are rendered by the server, so this script never has
|
// Zero means unlimited.
|
||||||
// to ask who the visitor is. Zero means unlimited.
|
|
||||||
var maxSize = parseInt(form.dataset.maxSize, 10) || 0;
|
var maxSize = parseInt(form.dataset.maxSize, 10) || 0;
|
||||||
|
|
||||||
function formatSize(n) {
|
function formatSize(n) {
|
||||||
@@ -82,7 +76,6 @@
|
|||||||
errorBox.hidden = !msg;
|
errorBox.hidden = !msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- drag and drop ------------------------------------------------------
|
|
||||||
function describeSelection() {
|
function describeSelection() {
|
||||||
var f = fileInput.files[0];
|
var f = fileInput.files[0];
|
||||||
dropHint.textContent = f ? f.name + ' - ' + formatSize(f.size)
|
dropHint.textContent = f ? f.name + ' - ' + formatSize(f.size)
|
||||||
@@ -109,7 +102,6 @@
|
|||||||
});
|
});
|
||||||
fileInput.addEventListener('change', describeSelection);
|
fileInput.addEventListener('change', describeSelection);
|
||||||
|
|
||||||
// --- upload -------------------------------------------------------------
|
|
||||||
form.addEventListener('submit', function (e) {
|
form.addEventListener('submit', function (e) {
|
||||||
var file = fileInput.files[0];
|
var file = fileInput.files[0];
|
||||||
if (!file) return; // let the browser's own validation speak
|
if (!file) return; // let the browser's own validation speak
|
||||||
@@ -127,16 +119,15 @@
|
|||||||
progress.hidden = false;
|
progress.hidden = false;
|
||||||
|
|
||||||
// FormData follows markup order, not painted order, so the options and the
|
// FormData follows markup order, not painted order, so the options and the
|
||||||
// token precede the file part exactly as the server requires. The drop
|
// token precede the file part as the server requires. The drop zone only
|
||||||
// zone only looks like it comes first; see the order rules in style.css.
|
// looks like it comes first; see the order rules in style.css.
|
||||||
var data = new FormData(form);
|
var data = new FormData(form);
|
||||||
var started = Date.now();
|
var started = Date.now();
|
||||||
|
|
||||||
var xhr = new XMLHttpRequest();
|
var xhr = new XMLHttpRequest();
|
||||||
xhr.open('POST', form.action);
|
xhr.open('POST', form.action);
|
||||||
// Ask for the page a plain form post would have been given. What a
|
// Ask for the page a plain form post would get: the server owns what a
|
||||||
// finished upload looks like is the server's business, and it already
|
// finished upload looks like, and this only adds a progress bar.
|
||||||
// renders it; this script only adds the progress bar on the way there.
|
|
||||||
xhr.setRequestHeader('Accept', 'text/html');
|
xhr.setRequestHeader('Accept', 'text/html');
|
||||||
|
|
||||||
xhr.upload.onprogress = function (ev) {
|
xhr.upload.onprogress = function (ev) {
|
||||||
@@ -153,10 +144,9 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
xhr.onload = function () {
|
xhr.onload = function () {
|
||||||
// Whatever came back, the result page or an error page, is the page the
|
// Writing the response into this document rather than navigating leaves
|
||||||
// browser should be showing now. Writing it into this document rather
|
// the address on the form's URL, so a reload offers a fresh form instead
|
||||||
// than navigating leaves the address on the form's own URL, so a reload
|
// of posting the file again.
|
||||||
// offers a fresh form instead of posting the file a second time.
|
|
||||||
document.open();
|
document.open();
|
||||||
document.write(xhr.responseText);
|
document.write(xhr.responseText);
|
||||||
document.close();
|
document.close();
|
||||||
|
|||||||
+9
-23
@@ -83,10 +83,9 @@ input[type=text], input[type=password] {
|
|||||||
.row { display: flex; gap: 1rem; flex-wrap: wrap; }
|
.row { display: flex; gap: 1rem; flex-wrap: wrap; }
|
||||||
.row > .field { flex: 1 1 12rem; }
|
.row > .field { flex: 1 1 12rem; }
|
||||||
|
|
||||||
/* The file input has to be the last part in the submitted body, so it sits at
|
/* The file input must be the last part in the submitted body, so it sits last
|
||||||
the bottom of the form's markup and is lifted back to the top here. Only the
|
in the markup and is lifted back to the top here. Only the painting order
|
||||||
painting order moves; the submission order, which the server depends on, is
|
moves; the server depends on the markup's. */
|
||||||
the markup's. */
|
|
||||||
#upload { display: flex; flex-direction: column; }
|
#upload { display: flex; flex-direction: column; }
|
||||||
#upload > .drop { order: -1; }
|
#upload > .drop { order: -1; }
|
||||||
#upload > button { align-self: flex-start; }
|
#upload > button { align-self: flex-start; }
|
||||||
@@ -184,17 +183,13 @@ button.link {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Result card ------------------------------------------------------------ */
|
|
||||||
|
|
||||||
.result .links { margin: 1.25rem 0 0; }
|
.result .links { margin: 1.25rem 0 0; }
|
||||||
.result .links .field { margin-bottom: 1rem; }
|
.result .links .field { margin-bottom: 1rem; }
|
||||||
|
|
||||||
/* A read-only URL with its copy button alongside. */
|
|
||||||
.copyrow { display: flex; gap: .5rem; align-items: stretch; }
|
.copyrow { display: flex; gap: .5rem; align-items: stretch; }
|
||||||
.copyrow input { flex: 1 1 auto; min-width: 0; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: .8125rem; }
|
.copyrow input { flex: 1 1 auto; min-width: 0; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: .8125rem; }
|
||||||
.copyrow .copy { flex: 0 0 auto; }
|
.copyrow .copy { flex: 0 0 auto; }
|
||||||
|
|
||||||
/* Keep the detail list clear of the copy buttons above it. */
|
|
||||||
.result dl { margin-top: 1.75rem; padding-top: 1.25rem; border-top: 1px solid var(--line); }
|
.result dl { margin-top: 1.75rem; padding-top: 1.25rem; border-top: 1px solid var(--line); }
|
||||||
.result dl dd { margin-bottom: .25rem; }
|
.result dl dd { margin-bottom: .25rem; }
|
||||||
|
|
||||||
@@ -205,8 +200,6 @@ button.link {
|
|||||||
.copyrow input { flex-basis: 100%; }
|
.copyrow input { flex-basis: 100%; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Administration --------------------------------------------------------- */
|
|
||||||
|
|
||||||
header { display: flex; align-items: baseline; gap: 1rem; flex-wrap: wrap; }
|
header { display: flex; align-items: baseline; gap: 1rem; flex-wrap: wrap; }
|
||||||
header nav {
|
header nav {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
@@ -244,8 +237,6 @@ table.admin .nowrap { white-space: nowrap; }
|
|||||||
table.admin form { margin: 0; }
|
table.admin form { margin: 0; }
|
||||||
table.admin .actions-cell { text-align: right; width: 1%; }
|
table.admin .actions-cell { text-align: right; width: 1%; }
|
||||||
|
|
||||||
/* The filename carries the link and the identifier sits under it, which saves
|
|
||||||
a whole column of UUID. */
|
|
||||||
table.admin .filecell a { overflow-wrap: anywhere; font-weight: 500; }
|
table.admin .filecell a { overflow-wrap: anywhere; font-weight: 500; }
|
||||||
table.admin .filecell .id {
|
table.admin .filecell .id {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -264,8 +255,8 @@ button.small { padding: .2rem .5rem; font-size: .75rem; }
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Narrow screens get one card per row rather than a horizontal scrollbar,
|
/* One card per row beats a horizontal scrollbar, which is unusable for picking
|
||||||
which is unusable for picking a row and pressing a button in it. */
|
a row and pressing a button in it. */
|
||||||
@media (max-width: 46rem) {
|
@media (max-width: 46rem) {
|
||||||
table.admin,
|
table.admin,
|
||||||
table.admin tbody,
|
table.admin tbody,
|
||||||
@@ -289,10 +280,8 @@ button.small { padding: .2rem .5rem; font-size: .75rem; }
|
|||||||
|
|
||||||
.cli code, .hint code { background: var(--bg); padding: .1rem .3rem; border-radius: 4px; }
|
.cli code, .hint code { background: var(--bg); padding: .1rem .3rem; border-radius: 4px; }
|
||||||
|
|
||||||
/* Disclosure panels: the one-off token on the upload form, "Remove this file"
|
/* Disclosure panels. Open, a panel needs an edge, or there is no telling where
|
||||||
on the info page. Closed, a panel is a single line and needs nothing; open,
|
its contents end. */
|
||||||
it needs an edge, or its contents read as loose page furniture and there is
|
|
||||||
no telling where it ends. */
|
|
||||||
details.panel {
|
details.panel {
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
@@ -309,20 +298,17 @@ details.panel[open] > summary {
|
|||||||
border-bottom: 1px solid var(--line);
|
border-bottom: 1px solid var(--line);
|
||||||
margin-bottom: .75rem;
|
margin-bottom: .75rem;
|
||||||
}
|
}
|
||||||
/* The summary carries the panel's padding itself; everything else is inset to
|
|
||||||
match, and the panel closes the gap under the last of them. */
|
|
||||||
details.panel > :not(summary) { padding-left: .75rem; padding-right: .75rem; }
|
details.panel > :not(summary) { padding-left: .75rem; padding-right: .75rem; }
|
||||||
details.panel[open] { padding-bottom: .75rem; }
|
details.panel[open] { padding-bottom: .75rem; }
|
||||||
details.panel > :last-child { margin-bottom: 0; }
|
details.panel > :last-child { margin-bottom: 0; }
|
||||||
details.panel .field { max-width: 24rem; }
|
details.panel .field { max-width: 24rem; }
|
||||||
|
|
||||||
/* A hint that introduces a control is an instruction for what follows, not a
|
/* A hint before a control explains what follows, so it must not sit against
|
||||||
caption for what came before, so it must not sit against it. */
|
it. */
|
||||||
.hint + form, .hint + .field { margin-top: .75rem; }
|
.hint + form, .hint + .field { margin-top: .75rem; }
|
||||||
|
|
||||||
.limits .hint { margin-top: .75rem; }
|
.limits .hint { margin-top: .75rem; }
|
||||||
.limits dl, .card dl { row-gap: .375rem; }
|
.limits dl, .card dl { row-gap: .375rem; }
|
||||||
|
|
||||||
/* Actions that mix a link-button with a form-button. */
|
|
||||||
.actions { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }
|
.actions { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }
|
||||||
.actions form { margin: 0; }
|
.actions form { margin: 0; }
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
<form id="upload" class="card" method="post" action="{{.Base}}upload"
|
<form id="upload" class="card" method="post" action="{{.Base}}upload"
|
||||||
enctype="multipart/form-data" data-max-size="{{.MaxSizeBytes}}">
|
enctype="multipart/form-data" data-max-size="{{.MaxSizeBytes}}">
|
||||||
|
|
||||||
<!-- Field order is load-bearing: the server streams this body rather than
|
{{- /* Field order is load-bearing: the server streams this body rather than
|
||||||
spooling it, so every option and the token have to arrive before the
|
spooling it, so every option and the token have to arrive before the
|
||||||
file part does. The drop zone is lifted back to the top of the card with
|
file part does. The drop zone is lifted back to the top of the card with
|
||||||
CSS order, which changes what is seen without changing what is sent. -->
|
CSS order, which changes what is seen without changing what is sent. */}}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label class="field">
|
<label class="field">
|
||||||
@@ -32,10 +32,10 @@
|
|||||||
</p>
|
</p>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Token</span>
|
<span>Token</span>
|
||||||
<!-- Plain text on purpose: this is pasted once, behind a collapsed
|
{{- /* Plain text on purpose: this is pasted once, behind a collapsed
|
||||||
panel, and a masked field only hides the typo. Keeping it out of
|
panel, and a masked field only hides the typo. Keeping it out of
|
||||||
the password family also stops the browser treating this form as a
|
the password family also stops the browser treating this form as a
|
||||||
login and hunting for a username to fill in above. -->
|
login and hunting for a username to fill in above. */}}
|
||||||
<input type="text" name="token" autocomplete="off"
|
<input type="text" name="token" autocomplete="off"
|
||||||
spellcheck="false" autocapitalize="off">
|
spellcheck="false" autocapitalize="off">
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -40,8 +40,8 @@
|
|||||||
<input type="hidden" name="from" value="info">
|
<input type="hidden" name="from" value="info">
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Delete token</span>
|
<span>Delete token</span>
|
||||||
<!-- Plain text, like the upload form's: a delete token is pasted
|
{{- /* Plain text, like the upload form's: a delete token is pasted
|
||||||
once and a mask would only hide a mistake in it. -->
|
once and a mask would only hide a mistake in it. */}}
|
||||||
<input type="text" name="token" autocomplete="off" required
|
<input type="text" name="token" autocomplete="off" required
|
||||||
spellcheck="false" autocapitalize="off">
|
spellcheck="false" autocapitalize="off">
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -11,6 +11,9 @@
|
|||||||
<header>
|
<header>
|
||||||
<a class="brand" href="{{.Base}}">Uncensored Send</a>
|
<a class="brand" href="{{.Base}}">Uncensored Send</a>
|
||||||
<nav>
|
<nav>
|
||||||
|
{{- /* Uploading is what the site is for, so it is a named destination and
|
||||||
|
not only a click on the wordmark. */}}
|
||||||
|
<a href="{{.Base}}">Upload</a>
|
||||||
{{if .User}}
|
{{if .User}}
|
||||||
<a href="{{.Base}}files">{{if .Admin}}Administration{{else}}My files{{end}}</a>
|
<a href="{{.Base}}files">{{if .Admin}}Administration{{else}}My files{{end}}</a>
|
||||||
<span class="who">{{.User}}</span>
|
<span class="who">{{.User}}</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user