From 1246a277c17d8b600f9c9b1de7a12ae6fb6da962 Mon Sep 17 00:00:00 2001 From: steering7253 Date: Wed, 24 Jun 2026 03:22:20 -0400 Subject: init --- static/script.js | 178 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ static/style.css | 55 +++++++++++++++++ 2 files changed, 233 insertions(+) create mode 100644 static/script.js create mode 100644 static/style.css (limited to 'static') diff --git a/static/script.js b/static/script.js new file mode 100644 index 0000000..5d48603 --- /dev/null +++ b/static/script.js @@ -0,0 +1,178 @@ +/* +sources: +MDN: + https://developer.mozilla.org/en-US/docs/Web/API/File_API/Using_files_from_web_applications#example_uploading_a_user-selected_file + https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/File_drag_and_drop +web.dev: + https://web.dev/patterns/clipboard/paste-files/ +note that this likely means the code is mixed-licensed under: + MDN: "Attributions and copyright licensing" by Mozilla Contributors, licensed under CC-BY-SA 2.5. + web.dev: code samples are licensed under the Apache 2.0 License. For details, see the Google Developers Site Policies +*/ + +addEventListener("DOMContentLoaded", function () { + function uploadFiles(files) { + for (const file of files) { + const li = document.createElement("li"); + if (file.type.startsWith("image/")) { + const img = document.createElement("img"); + img.src = URL.createObjectURL(file); + img.alt = file.name; + li.appendChild(img); + } + li.appendChild(document.createTextNode("Uploading " + file.name + "...")); + preview.appendChild(li); + + new FileUpload(li, file) + } + } + + /* DRAG-N-DROP */ + const dropZone = document.getElementById("drop-zone"); + const preview = document.getElementById("preview"); + + function dropHandler(ev) { + ev.preventDefault(); + const files = [...ev.dataTransfer.items] + .map((item) => item.getAsFile()) + .filter((file) => file); + uploadFiles(files); + } + + dropZone.addEventListener("drop", dropHandler); + + dropZone.addEventListener("dragover", (e) => { + const fileItems = [...e.dataTransfer.items].filter( + (item) => item.kind === "file", + ); + if (fileItems.length > 0) { + e.preventDefault(); + if (fileItems.some((item) => item.type.startsWith("image/"))) { + e.dataTransfer.dropEffect = "copy"; + } else { + e.dataTransfer.dropEffect = "none"; + } + } + }); + + window.addEventListener("dragover", (e) => { + const fileItems = [...e.dataTransfer.items].filter( + (item) => item.kind === "file", + ); + if (fileItems.length > 0) { + e.preventDefault(); + if (!dropZone.contains(e.target)) { + e.dataTransfer.dropEffect = "none"; + } + } + }); + + const fileInput = document.getElementById("file-input"); + fileInput.addEventListener("change", (e) => { + uploadFiles(e.target.files); + }); + + window.addEventListener("drop", (e) => { + if ([...e.dataTransfer.items].some((item) => item.kind === "file")) { + e.preventDefault(); + } + }); + + + + /* PASTE */ + document.addEventListener('paste', async (e) => { + // Prevent the default behavior, so you can code your own logic. + e.preventDefault(); + if (!e.clipboardData.files.length) { + return; + } + // Iterate over all pasted files. + uploadFiles(e.clipboardData.files) + }); + + + + /* EXPANDER */ + document.querySelectorAll('.expander-toggle').forEach((e) => { + e.addEventListener('click', toggle_expand); + }); +}); + +function toggle_expand(ev) { + const cl = this.parentNode.querySelector('.expander').classList; + const icon = this.parentNode.querySelector('.expander-icon'); + if (cl.contains('expander-hidden')) { + cl.replace('expander-hidden', 'expander-shown'); + icon.innerHTML = "▼"; + } else { + cl.remove('expander-shown'); + cl.add('expander-hidden'); + icon.innerHTML = "►"; + } +} + +function FileUpload(preview, file) { + this.ctrl = createThrobber(preview); + const xhr = new XMLHttpRequest(); + this.xhr = xhr; + + this.xhr.upload.addEventListener("progress", (e) => { + if (e.lengthComputable) { + const percentage = Math.round((e.loaded * 100) / e.total); + this.ctrl.update(percentage); + } + }); + + xhr.upload.addEventListener("load", (e) => { + this.ctrl.update(100); + preview.removeChild(this.ctrl); + }); + xhr.addEventListener("load", (e) => { + const a = document.createElement('a'); + const text = document.createTextNode(xhr.responseText); + a.href = xhr.responseText; + a.appendChild(text); + preview.appendChild(a); + }); + xhr.open("POST", ascend(preview, "form").action + "/" + file.name); + xhr.overrideMimeType("application/octet-stream; charset=x-user-defined-binary"); + xhr.send(file); +} + +function createThrobber(element) { + const throbberWidth = 64; + const throbberHeight = 6; + const throbber = document.createElement("canvas"); + throbber.classList.add("upload-progress"); + throbber.setAttribute("width", throbberWidth); + throbber.setAttribute("height", throbberHeight); + element.appendChild(throbber); + throbber.ctx = throbber.getContext("2d"); + throbber.ctx.fillStyle = "orange"; + throbber.update = (percent) => { + throbber.ctx.fillRect( + 0, + 0, + (throbberWidth * percent) / 100, + throbberHeight, + ); + if (percent === 100) { + throbber.ctx.fillStyle = "green"; + } + }; + throbber.update(0); + return throbber; +} + +function ascend(from_element, to_tag_name) { + to_tag_name = to_tag_name.toLowerCase(); + e = from_element; + + while (e && e.parentNode) { + e = e.parentNode; + if (e.tagName && e.tagName.toLowerCase() == to_tag_name) { + return e; + } + } +} diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..b2584d7 --- /dev/null +++ b/static/style.css @@ -0,0 +1,55 @@ +body { + font-family: "Arial", sans-serif; +} + +#drop-zone { + display: flex; + align-items: center; + justify-content: center; + width: 500px; + max-width: 100%; + height: 200px; + padding: 1em; + border: 1px solid #cccccc; + border-radius: 4px; + color: slategray; + cursor: pointer; +} + +#file-input { + display: none; +} + +#preview { + display: flex; + flex-direction: column; + gap: 0.5em; + list-style: none; + padding: 0; +} + +#preview li { + display: flex; + align-items: center; + gap: 0.5em; + margin: 0; + width: 100%; + height: 100px; +} + +#preview img { + width: 100px; + height: 100px; + object-fit: cover; +} + +.upload-progress { + border: 1px solid black; +} + +.expander-hidden { + display: none; +} +.expander-toggle { + cursor: pointer; +} -- cgit v1.3.1-10-gc9f91