summaryrefslogtreecommitdiff
path: root/static
diff options
context:
space:
mode:
Diffstat (limited to 'static')
-rw-r--r--static/script.js178
-rw-r--r--static/style.css55
2 files changed, 233 insertions, 0 deletions
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;
+}