From fe32c53a1789b28883069cb492e1eb2a8c42c22f Mon Sep 17 00:00:00 2001 From: Drew DeVault Date: Fri, 13 Nov 2020 11:05:10 -0500 Subject: [PATCH] Improve UI while emails are being sent --- themes/alps/assets/attachments.js | 188 ------------------------------------ themes/alps/assets/compose.js | 196 ++++++++++++++++++++++++++++++++++++++ themes/alps/assets/style.css | 40 +++++++- themes/alps/compose.html | 12 ++- 4 files changed, 244 insertions(+), 192 deletions(-) delete mode 100644 themes/alps/assets/attachments.js create mode 100644 themes/alps/assets/compose.js diff --git a/themes/alps/assets/attachments.js b/themes/alps/assets/attachments.js deleted file mode 100644 index dc6cc04..0000000 --- a/themes/alps/assets/attachments.js +++ /dev/null @@ -1,188 +0,0 @@ -let attachments = []; - -const headers = document.querySelector(".create-update .headers"); -headers.classList.remove("no-js"); - -const attachmentsNode = document.getElementById("attachment-list"); -attachmentsNode.style.display = ''; -const helpNode = attachmentsNode.querySelector(".help"); - -const attachmentsInput = headers.querySelector("input[type='file']"); -attachmentsInput.removeAttribute("name"); -attachmentsInput.addEventListener("input", ev => { - const files = attachmentsInput.files; - for (let i = 0; i < files.length; i++) { - attachFile(files[i]); - } -}); - -window.addEventListener("dragenter", dragNOP); -window.addEventListener("dragleave", dragNOP); -window.addEventListener("dragover", dragNOP); - -window.addEventListener("drop", ev => { - ev.preventDefault(); - const files = ev.dataTransfer.files; - for (let i = 0; i < files.length; i++) { - attachFile(files[i]); - } -}); - -function dragNOP(e) { - e.stopPropagation(); - e.preventDefault(); -} - -const sendButton = document.getElementById("send-button"), - saveButton = document.getElementById("save-button"); - -const attachmentUUIDsNode = document.getElementById("attachment-uuids"); -function updateState() { - let complete = true; - for (let i = 0; i < attachments.length; i++) { - const a = attachments[i]; - const progress = a.node.querySelector(".progress"); - progress.style.width = `${Math.floor(a.progress * 100)}%`; - complete &= a.progress === 1.0; - if (a.progress === 1.0) { - progress.style.display = 'none'; - } - } - - if (complete) { - sendButton.removeAttribute("disabled"); - saveButton.removeAttribute("disabled"); - } else { - sendButton.setAttribute("disabled", "disabled"); - saveButton.setAttribute("disabled", "disabled"); - } - - attachmentUUIDsNode.value = attachments. - filter(a => a.progress === 1.0). - map(a => a.uuid). - join(","); -} - -function attachFile(file) { - helpNode.remove(); - - const xhr = new XMLHttpRequest(); - const node = attachmentNodeFor(file); - const attachment = { - node: node, - progress: 0, - xhr: xhr, - }; - attachments.push(attachment); - attachmentsNode.appendChild(node); - node.querySelector("button").addEventListener("click", ev => { - attachment.xhr.abort(); - attachments = attachments.filter(a => a !== attachment); - node.remove(); - updateState(); - - if (typeof attachment.uuid !== "undefined") { - const cancel = new XMLHttpRequest(); - cancel.open("POST", `/compose/attachment/${attachment.uuid}/remove`); - cancel.send(); - } - }); - - let formData = new FormData(); - formData.append("attachments", file); - - const handleError = msg => { - attachments = attachments.filter(a => a !== attachment); - node.classList.add("error"); - node.querySelector(".progress").remove(); - node.querySelector(".size").remove(); - node.querySelector("button").remove(); - node.querySelector(".error").innerText = "Error: " + msg; - updateState(); - }; - - xhr.open("POST", "/compose/attachment"); - xhr.upload.addEventListener("progress", ev => { - attachment.progress = ev.loaded / ev.total; - updateState(); - }); - xhr.addEventListener("load", () => { - let resp; - try { - resp = JSON.parse(xhr.responseText); - } catch { - resp = { "error": "Error: invalid response" }; - } - - if (xhr.status !== 200) { - handleError(resp["error"]); - return; - } - - attachment.uuid = resp[0]; - updateState(); - }); - xhr.addEventListener("error", () => { - handleError("an unexpected problem occured"); - }); - xhr.send(formData); - - updateState(); -} - -function attachmentNodeFor(file) { - const node = document.createElement("div"), - progress = document.createElement("span"), - filename = document.createElement("span"), - error = document.createElement("span"), - size = document.createElement("span"), - button = document.createElement("button"); - node.classList.add("upload"); - - progress.classList.add("progress"); - node.appendChild(progress); - - filename.classList.add("filename"); - filename.innerText = file.name; - node.appendChild(filename); - - error.classList.add("error"); - node.appendChild(error); - - size.classList.add("size"); - size.innerText = formatSI(file.size) + "B"; - node.appendChild(size); - - button.innerHTML = "×"; - node.appendChild(button); - return node; -} - -// via https://github.com/ThomWright/format-si-prefix; MIT license -// Copyright (c) 2015 Thom Wright -const PREFIXES = { - '24': 'Y', '21': 'Z', '18': 'E', '15': 'P', '12': 'T', '9': 'G', '6': 'M', - '3': 'k', '0': '', '-3': 'm', '-6': 'µ', '-9': 'n', '-12': 'p', '-15': 'f', - '-18': 'a', '-21': 'z', '-24': 'y' -}; - -function formatSI(num) { - if (num === 0) { - return '0'; - } - let sig = Math.abs(num); // significand - let exponent = 0; - while (sig >= 1000 && exponent < 24) { - sig /= 1000; - exponent += 3; - } - while (sig < 1 && exponent > -24) { - sig *= 1000; - exponent -= 3; - } - const signPrefix = num < 0 ? '-' : ''; - if (sig > 1000) { - return signPrefix + sig.toFixed(0) + PREFIXES[exponent]; - } - return signPrefix + parseFloat(sig.toPrecision(3)) + PREFIXES[exponent]; -} diff --git a/themes/alps/assets/compose.js b/themes/alps/assets/compose.js new file mode 100644 index 0000000..e6f057f --- /dev/null +++ b/themes/alps/assets/compose.js @@ -0,0 +1,196 @@ +const composeForm = document.getElementById("compose-form"); +const sendProgress = document.getElementById("send-progress"); +composeForm.addEventListener("submit", ev => { + [...document.querySelectorAll("input, textarea")].map( + i => i.setAttribute("readonly", "readonly")); + sendProgress.style.display = 'flex'; +}); + +let attachments = []; + +const headers = document.querySelector(".create-update .headers"); +headers.classList.remove("no-js"); + +const attachmentsNode = document.getElementById("attachment-list"); +attachmentsNode.style.display = ''; +const helpNode = attachmentsNode.querySelector(".help"); + +const attachmentsInput = headers.querySelector("input[type='file']"); +attachmentsInput.removeAttribute("name"); +attachmentsInput.addEventListener("input", ev => { + const files = attachmentsInput.files; + for (let i = 0; i < files.length; i++) { + attachFile(files[i]); + } +}); + +window.addEventListener("dragenter", dragNOP); +window.addEventListener("dragleave", dragNOP); +window.addEventListener("dragover", dragNOP); + +window.addEventListener("drop", ev => { + ev.preventDefault(); + const files = ev.dataTransfer.files; + for (let i = 0; i < files.length; i++) { + attachFile(files[i]); + } +}); + +function dragNOP(e) { + e.stopPropagation(); + e.preventDefault(); +} + +const sendButton = document.getElementById("send-button"), + saveButton = document.getElementById("save-button"); + +const attachmentUUIDsNode = document.getElementById("attachment-uuids"); +function updateState() { + let complete = true; + for (let i = 0; i < attachments.length; i++) { + const a = attachments[i]; + const progress = a.node.querySelector(".progress"); + progress.style.width = `${Math.floor(a.progress * 100)}%`; + complete &= a.progress === 1.0; + if (a.progress === 1.0) { + progress.style.display = 'none'; + } + } + + if (complete) { + sendButton.removeAttribute("disabled"); + saveButton.removeAttribute("disabled"); + } else { + sendButton.setAttribute("disabled", "disabled"); + saveButton.setAttribute("disabled", "disabled"); + } + + attachmentUUIDsNode.value = attachments. + filter(a => a.progress === 1.0). + map(a => a.uuid). + join(","); +} + +function attachFile(file) { + helpNode.remove(); + + const xhr = new XMLHttpRequest(); + const node = attachmentNodeFor(file); + const attachment = { + node: node, + progress: 0, + xhr: xhr, + }; + attachments.push(attachment); + attachmentsNode.appendChild(node); + node.querySelector("button").addEventListener("click", ev => { + attachment.xhr.abort(); + attachments = attachments.filter(a => a !== attachment); + node.remove(); + updateState(); + + if (typeof attachment.uuid !== "undefined") { + const cancel = new XMLHttpRequest(); + cancel.open("POST", `/compose/attachment/${attachment.uuid}/remove`); + cancel.send(); + } + }); + + let formData = new FormData(); + formData.append("attachments", file); + + const handleError = msg => { + attachments = attachments.filter(a => a !== attachment); + node.classList.add("error"); + node.querySelector(".progress").remove(); + node.querySelector(".size").remove(); + node.querySelector("button").remove(); + node.querySelector(".error").innerText = "Error: " + msg; + updateState(); + }; + + xhr.open("POST", "/compose/attachment"); + xhr.upload.addEventListener("progress", ev => { + attachment.progress = ev.loaded / ev.total; + updateState(); + }); + xhr.addEventListener("load", () => { + let resp; + try { + resp = JSON.parse(xhr.responseText); + } catch { + resp = { "error": "Error: invalid response" }; + } + + if (xhr.status !== 200) { + handleError(resp["error"]); + return; + } + + attachment.uuid = resp[0]; + updateState(); + }); + xhr.addEventListener("error", () => { + handleError("an unexpected problem occured"); + }); + xhr.send(formData); + + updateState(); +} + +function attachmentNodeFor(file) { + const node = document.createElement("div"), + progress = document.createElement("span"), + filename = document.createElement("span"), + error = document.createElement("span"), + size = document.createElement("span"), + button = document.createElement("button"); + node.classList.add("upload"); + + progress.classList.add("progress"); + node.appendChild(progress); + + filename.classList.add("filename"); + filename.innerText = file.name; + node.appendChild(filename); + + error.classList.add("error"); + node.appendChild(error); + + size.classList.add("size"); + size.innerText = formatSI(file.size) + "B"; + node.appendChild(size); + + button.innerHTML = "×"; + node.appendChild(button); + return node; +} + +// via https://github.com/ThomWright/format-si-prefix; MIT license +// Copyright (c) 2015 Thom Wright +const PREFIXES = { + '24': 'Y', '21': 'Z', '18': 'E', '15': 'P', '12': 'T', '9': 'G', '6': 'M', + '3': 'k', '0': '', '-3': 'm', '-6': 'µ', '-9': 'n', '-12': 'p', '-15': 'f', + '-18': 'a', '-21': 'z', '-24': 'y' +}; + +function formatSI(num) { + if (num === 0) { + return '0'; + } + let sig = Math.abs(num); // significand + let exponent = 0; + while (sig >= 1000 && exponent < 24) { + sig /= 1000; + exponent += 3; + } + while (sig < 1 && exponent > -24) { + sig *= 1000; + exponent -= 3; + } + const signPrefix = num < 0 ? '-' : ''; + if (sig > 1000) { + return signPrefix + sig.toFixed(0) + PREFIXES[exponent]; + } + return signPrefix + parseFloat(sig.toPrecision(3)) + PREFIXES[exponent]; +} diff --git a/themes/alps/assets/style.css b/themes/alps/assets/style.css index 79bc88a..830506f 100644 --- a/themes/alps/assets/style.css +++ b/themes/alps/assets/style.css @@ -254,10 +254,48 @@ main.create-update #attachment-list .upload.error .error { color: red; } -main.create-update textarea { +main.create-update .text { flex: 1 auto; resize: none; margin-top: 1rem; + position: relative; +} + +main.create-update textarea { + width: 100%; + height: 100%; +} + +#send-progress { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + font-size: 1.2rem; + background: rgba(0, 0, 0, 0.2); + display: flex; + align-items: center; + justify-content: center; +} + +#send-progress svg { + height: 1.2rem; + margin-right: 0.3rem; + animation: fa-spin 2s infinite linear; +} + +#send-progress svg path { + fill: currentColor; +} + +@keyframes fa-spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(1turn); + } } main table { border-collapse: collapse; width: 100%; border: 1px solid #eee; } diff --git a/themes/alps/compose.html b/themes/alps/compose.html index a8b6815..4c272c5 100644 --- a/themes/alps/compose.html +++ b/themes/alps/compose.html @@ -8,7 +8,7 @@
-
+ @@ -62,7 +62,13 @@
- +
+ + +
{{range .Extra.EmailSuggestions}} @@ -80,6 +86,6 @@ - + {{template "foot.html"}}