From e66055de08acde23c32480dace7b8bce428071a1 Mon Sep 17 00:00:00 2001 From: MatthewHana Date: Sun, 17 Mar 2024 12:30:15 +1100 Subject: [PATCH 1/6] WEB UI: HREF for Upload, Refresh button, and CSS fixes --- radicale/web/internal_data/css/loading.svg | 125 ++++++----- radicale/web/internal_data/css/main.css | 42 +++- radicale/web/internal_data/fn.js | 240 +++++++++++++-------- radicale/web/internal_data/index.html | 15 +- 4 files changed, 268 insertions(+), 154 deletions(-) diff --git a/radicale/web/internal_data/css/loading.svg b/radicale/web/internal_data/css/loading.svg index 5151c1b6..3513ff67 100644 --- a/radicale/web/internal_data/css/loading.svg +++ b/radicale/web/internal_data/css/loading.svg @@ -1,55 +1,72 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/radicale/web/internal_data/css/main.css b/radicale/web/internal_data/css/main.css index 0504018d..a6d7da72 100644 --- a/radicale/web/internal_data/css/main.css +++ b/radicale/web/internal_data/css/main.css @@ -103,11 +103,20 @@ main{ color: white; text-decoration: none; padding: 3px 10px; - position: absolute; - right: 25px; + position: relative; border-radius: 4px; } +#logoutview a[data-name=logout]{ + right: 25px; + float: right; +} + +#logoutview a[data-name=refresh]{ + left: 25px; + float: left; +} + #collectionsscene{ display: flex; flex-direction: row; @@ -116,11 +125,11 @@ main{ align-items: center; margin-top: 50px; width: 100%; - height: calc(100vh - 50px); + height: 100vh; } #collectionsscene article{ - width: 250px; + width: 275px; background: rgb(250, 250, 250); border-radius: 8px; box-shadow: 2px 2px 3px #0000001a; @@ -129,7 +138,7 @@ main{ padding-top: 0; margin: 10px; float: left; - height: 350px; + min-height: 375px; overflow: hidden; } @@ -170,11 +179,12 @@ main{ } #collectionsscene article:hover ul{ - display: flex !important; + visibility: visible; } #collectionsscene ul{ - display: none; + visibility: hidden; + display: flex; justify-content: space-evenly; width: 60%; margin: 0 20%; @@ -216,7 +226,7 @@ main{ #uploadcollectionscene ul{ margin: 10px -30px; max-height: 600px; - overflow: overlay; + overflow-y: scroll; } #uploadcollectionscene li{ @@ -225,6 +235,11 @@ main{ padding-bottom: 10px; } +#uploadcollectionscene div[data-name=pending]{ + width: 100%; + text-align: center; +} + #uploadcollectionscene .successmessage{ color: #4e9a06; width: 100%; @@ -291,6 +306,11 @@ main{ padding-top: 15px; } +img.loading{ + width: 150px; + height: 150px; +} + .error::before{ content: "!"; height: 1em; @@ -391,7 +411,7 @@ button.blue:active, a.blue:active{ #collectionsscene article{ height: auto; - min-height: 350px; + min-height: 375px; } .container{ @@ -399,10 +419,10 @@ button.blue:active, a.blue:active{ } #collectionsscene ul{ - display: flex !important; + visibility: visible !important; } #logoutview span{ - text-align: left; + padding: 0 5px; } } diff --git a/radicale/web/internal_data/fn.js b/radicale/web/internal_data/fn.js index 37a1ee22..c2dd1458 100644 --- a/radicale/web/internal_data/fn.js +++ b/radicale/web/internal_data/fn.js @@ -542,7 +542,8 @@ function LoginScene() { let error_form = html_scene.querySelector("[data-name=error]"); let logout_view = document.getElementById("logoutview"); let logout_user_form = logout_view.querySelector("[data-name=user]"); - let logout_btn = logout_view.querySelector("[data-name=link]"); + let logout_btn = logout_view.querySelector("[data-name=logout]"); + let refresh_btn = logout_view.querySelector("[data-name=refresh]"); /** @type {?number} */ let scene_index = null; let user = ""; @@ -573,6 +574,7 @@ function LoginScene() { // setup logout logout_view.classList.remove("hidden"); logout_btn.onclick = onlogout; + refresh_btn.onclick = refresh; logout_user_form.textContent = user + "'s Collections"; // Fetch principal let loading_scene = new LoadingScene(); @@ -623,9 +625,17 @@ function LoginScene() { function remove_logout() { logout_view.classList.add("hidden"); logout_btn.onclick = null; + refresh_btn.onclick = null; logout_user_form.textContent = ""; } + function refresh(){ + //The easiest way to refresh is to push a LoadingScene onto the stack and then pop it + //forcing the scene below it, the Collections Scene to refresh itself. + push_scene(new LoadingScene(), false); + pop_scene(scene_stack.length-2); + } + this.show = function() { remove_logout(); fill_form(); @@ -684,12 +694,6 @@ function CollectionsScene(user, password, collection, onerror) { /** @type {?XMLHttpRequest} */ let collections_req = null; /** @type {?Array} */ let collections = null; /** @type {Array} */ let nodes = []; - let filesInput = document.createElement("input"); - filesInput.setAttribute("type", "file"); - filesInput.setAttribute("accept", ".ics, .vcf"); - filesInput.setAttribute("multiple", ""); - let filesInputForm = document.createElement("form"); - filesInputForm.appendChild(filesInput); function onnew() { try { @@ -702,17 +706,9 @@ function CollectionsScene(user, password, collection, onerror) { } function onupload() { - filesInput.click(); - return false; - } - - function onfileschange() { try { - let files = filesInput.files; - if (files.length > 0) { - let upload_scene = new UploadCollectionScene(user, password, collection, files); - push_scene(upload_scene); - } + let upload_scene = new UploadCollectionScene(user, password, collection); + push_scene(upload_scene); } catch(err) { console.error(err); } @@ -740,6 +736,9 @@ function CollectionsScene(user, password, collection, onerror) { } function show_collections(collections) { + let heightOfNavBar = document.querySelector("#logoutview").offsetHeight + "px"; + html_scene.style.marginTop = heightOfNavBar; + html_scene.style.height = "calc(100vh - " + heightOfNavBar +")"; collections.forEach(function (collection) { let node = template.cloneNode(true); node.classList.remove("hidden"); @@ -820,8 +819,6 @@ function CollectionsScene(user, password, collection, onerror) { html_scene.classList.remove("hidden"); new_btn.onclick = onnew; upload_btn.onclick = onupload; - filesInputForm.reset(); - filesInput.onchange = onfileschange; if (collections === null) { update(); } else { @@ -834,7 +831,6 @@ function CollectionsScene(user, password, collection, onerror) { scene_index = scene_stack.length - 1; new_btn.onclick = null; upload_btn.onclick = null; - filesInput.onchange = null; collections = null; // remove collection nodes.forEach(function(node) { @@ -849,7 +845,6 @@ function CollectionsScene(user, password, collection, onerror) { collections_req = null; } collections = null; - filesInputForm.reset(); }; } @@ -861,43 +856,89 @@ function CollectionsScene(user, password, collection, onerror) { * @param {Collection} collection parent collection * @param {Array} files */ -function UploadCollectionScene(user, password, collection, files) { +function UploadCollectionScene(user, password, collection) { let html_scene = document.getElementById("uploadcollectionscene"); let template = html_scene.querySelector("[data-name=filetemplate]"); + let upload_btn = html_scene.querySelector("[data-name=submit]"); let close_btn = html_scene.querySelector("[data-name=close]"); + let uploadfile_form = html_scene.querySelector("[data-name=uploadfile]"); + let uploadfile_lbl = html_scene.querySelector("label[for=uploadfile]"); + let href_form = html_scene.querySelector("[data-name=href]"); + let href_label = html_scene.querySelector("label[for=href]"); + let hreflimitmsg_html = html_scene.querySelector("[data-name=hreflimitmsg]"); + let pending_html = html_scene.querySelector("[data-name=pending]"); + + let files = uploadfile_form.files; + href_form.addEventListener("keydown", cleanHREFinput); + upload_btn.onclick = upload_start; + uploadfile_form.onchange = onfileschange; + + let href = random_uuid(); + href_form.value = href; /** @type {?number} */ let scene_index = null; /** @type {?XMLHttpRequest} */ let upload_req = null; - /** @type {Array} */ let errors = []; + /** @type {Array} */ let results = []; /** @type {?Array} */ let nodes = null; - function upload_next() { + function upload_start() { try { - if (files.length === errors.length) { - if (errors.every(error => error === null)) { - pop_scene(scene_index - 1); - } else { - close_btn.classList.remove("hidden"); - } - } else { - let file = files[errors.length]; - let upload_href = collection.href + random_uuid() + "/"; - upload_req = upload_collection(user, password, upload_href, file, function(error) { - if (scene_index === null) { - return; - } - upload_req = null; - errors.push(error); - updateFileStatus(errors.length - 1); - upload_next(); - }); + if(!read_form()){ + return false; } + uploadfile_form.classList.add("hidden"); + uploadfile_lbl.classList.add("hidden"); + href_form.classList.add("hidden"); + href_label.classList.add("hidden"); + hreflimitmsg_html.classList.add("hidden"); + upload_btn.classList.add("hidden"); + close_btn.classList.add("hidden"); + + pending_html.classList.remove("hidden"); + + nodes = []; + for (let i = 0; i < files.length; i++) { + let file = files[i]; + let node = template.cloneNode(true); + node.classList.remove("hidden"); + let name_form = node.querySelector("[data-name=name]"); + name_form.textContent = file.name; + node.classList.remove("hidden"); + nodes.push(node); + updateFileStatus(i); + template.parentNode.insertBefore(node, template); + } + upload_next(); } catch(err) { console.error(err); } return false; } + function upload_next(){ + try{ + if (files.length === results.length) { + pending_html.classList.add("hidden"); + close_btn.classList.remove("hidden"); + return; + } else { + let file = files[results.length]; + if(files.length > 1 || href.length == 0){ + href = random_uuid(); + } + let upload_href = collection.href + "/" + href + "/"; + upload_req = upload_collection(user, password, upload_href, file, function(result) { + upload_req = null; + results.push(result); + updateFileStatus(results.length - 1); + upload_next(); + }); + } + }catch(err){ + console.error(err); + } + } + function onclose() { try { pop_scene(scene_index - 1); @@ -911,54 +952,77 @@ function UploadCollectionScene(user, password, collection, files) { if (nodes === null) { return; } - let pending_form = nodes[i].querySelector("[data-name=pending]"); let success_form = nodes[i].querySelector("[data-name=success]"); let error_form = nodes[i].querySelector("[data-name=error]"); - if (errors.length > i) { - pending_form.classList.add("hidden"); - if (errors[i]) { + if (results.length > i) { + if (results[i]) { success_form.classList.add("hidden"); - error_form.textContent = "Error: " + errors[i]; + error_form.textContent = "Error: " + results[i]; error_form.classList.remove("hidden"); } else { success_form.classList.remove("hidden"); error_form.classList.add("hidden"); } } else { - pending_form.classList.remove("hidden"); success_form.classList.add("hidden"); error_form.classList.add("hidden"); } } + function read_form() { + cleanHREFinput(href_form); + let newhreftxtvalue = href_form.value.trim().toLowerCase(); + if(!isValidHREF(newhreftxtvalue)){ + alert("You must enter a valid HREF"); + return false; + } + href = newhreftxtvalue; + + if(uploadfile_form.files.length == 0){ + alert("You must select at least one file to upload"); + return false; + } + files = uploadfile_form.files; + return true; + } + + function onfileschange() { + files = uploadfile_form.files; + if(files.length > 1){ + hreflimitmsg_html.classList.remove("hidden"); + href_form.classList.add("hidden"); + href_label.classList.add("hidden"); + }else{ + hreflimitmsg_html.classList.add("hidden"); + href_form.classList.remove("hidden"); + href_label.classList.remove("hidden"); + } + return false; + } + this.show = function() { + scene_index = scene_stack.length - 1; html_scene.classList.remove("hidden"); - if (errors.length < files.length) { - close_btn.classList.add("hidden"); - } close_btn.onclick = onclose; - nodes = []; - for (let i = 0; i < files.length; i++) { - let file = files[i]; - let node = template.cloneNode(true); - node.classList.remove("hidden"); - let name_form = node.querySelector("[data-name=name]"); - name_form.textContent = file.name; - node.classList.remove("hidden"); - nodes.push(node); - updateFileStatus(i); - template.parentNode.insertBefore(node, template); - } - if (scene_index === null) { - scene_index = scene_stack.length - 1; - upload_next(); - } }; this.hide = function() { html_scene.classList.add("hidden"); close_btn.classList.remove("hidden"); + upload_btn.classList.remove("hidden"); + uploadfile_form.classList.remove("hidden"); + uploadfile_lbl.classList.remove("hidden"); + href_form.classList.remove("hidden"); + href_label.classList.remove("hidden"); + hreflimitmsg_html.classList.add("hidden"); + pending_html.classList.add("hidden"); close_btn.onclick = null; + upload_btn.onclick = null; + href_form.value = ""; + uploadfile_form.value = ""; + if(nodes == null){ + return; + } nodes.forEach(function(node) { node.parentNode.removeChild(node); }); @@ -1142,7 +1206,7 @@ function CreateEditCollectionScene(user, password, collection) { function read_form() { if(!edit){ - cleanHREFinput(); + cleanHREFinput(href_form); let newhreftxtvalue = href_form.value.trim().toLowerCase(); if(!isValidHREF(newhreftxtvalue)){ alert("You must enter a valid HREF"); @@ -1226,12 +1290,6 @@ function CreateEditCollectionScene(user, password, collection) { return false; } - function cleanHREFinput(event){ - let currentTxtVal = href_form.value.trim().toLowerCase(); - //Clean the HREF to remove non lowercase letters and dashes - currentTxtVal = currentTxtVal.replace(/(?![0-9a-z\-\_])./g, ''); - href_form.value = currentTxtVal; - } function onTypeChange(e){ if(type_form.value == CollectionType.WEBCAL){ @@ -1243,17 +1301,6 @@ function CreateEditCollectionScene(user, password, collection) { } } - function isValidHREF(href){ - if(href.length < 1){ - return false; - } - if(href.indexOf("/") != -1){ - return false; - } - - return true; - } - this.show = function() { this.release(); scene_index = scene_stack.length - 1; @@ -1304,7 +1351,28 @@ function bytesToHumanReadable(bytes, dp=1) { var i = bytes == 0 ? 0 : Math.floor(Math.log(bytes) / Math.log(1024)); return (bytes / Math.pow(1024, i)).toFixed(dp) * 1 + ' ' + ['b', 'kb', 'mb', 'gb', 'tb'][i]; } - + +function cleanHREFinput(a){ + let href_form = a; + if(a.target){ + href_form = a.target; + } + let currentTxtVal = href_form.value.trim().toLowerCase(); + //Clean the HREF to remove non lowercase letters and dashes + currentTxtVal = currentTxtVal.replace(/(?![0-9a-z\-\_])./g, ''); + href_form.value = currentTxtVal; +} + +function isValidHREF(href){ + if(href.length < 1){ + return false; + } + if(href.indexOf("/") != -1){ + return false; + } + + return true; +} function main() { // Hide startup loading message diff --git a/radicale/web/internal_data/index.html b/radicale/web/internal_data/index.html index 747fa472..e04d749b 100644 --- a/radicale/web/internal_data/index.html +++ b/radicale/web/internal_data/index.html @@ -14,12 +14,13 @@
- Loading + Loading...

Loading

Please wait...

@@ -155,12 +156,20 @@
+
+ + + + + +
From d05d726dc2cc801eb4e8c76dea4804b8aedfac82 Mon Sep 17 00:00:00 2001 From: MatthewHana Date: Mon, 18 Mar 2024 06:33:57 +1100 Subject: [PATCH 2/6] Spacing tweaks to try to resolve odd conflict --- radicale/web/internal_data/fn.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/radicale/web/internal_data/fn.js b/radicale/web/internal_data/fn.js index c2dd1458..401b243c 100644 --- a/radicale/web/internal_data/fn.js +++ b/radicale/web/internal_data/fn.js @@ -1352,9 +1352,9 @@ function bytesToHumanReadable(bytes, dp=1) { return (bytes / Math.pow(1024, i)).toFixed(dp) * 1 + ' ' + ['b', 'kb', 'mb', 'gb', 'tb'][i]; } -function cleanHREFinput(a){ +function cleanHREFinput(a) { let href_form = a; - if(a.target){ + if (a.target) { href_form = a.target; } let currentTxtVal = href_form.value.trim().toLowerCase(); @@ -1363,11 +1363,11 @@ function cleanHREFinput(a){ href_form.value = currentTxtVal; } -function isValidHREF(href){ - if(href.length < 1){ +function isValidHREF(href) { + if (href.length < 1) { return false; } - if(href.indexOf("/") != -1){ + if (href.indexOf("/") != -1) { return false; } From 4e9910522533f4bc1e998008b37628adaa2fd88c Mon Sep 17 00:00:00 2001 From: MatthewHana Date: Mon, 18 Mar 2024 06:35:07 +1100 Subject: [PATCH 3/6] Remove HREF validity functions [temporary] --- radicale/web/internal_data/fn.js | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/radicale/web/internal_data/fn.js b/radicale/web/internal_data/fn.js index 401b243c..85818473 100644 --- a/radicale/web/internal_data/fn.js +++ b/radicale/web/internal_data/fn.js @@ -1352,28 +1352,6 @@ function bytesToHumanReadable(bytes, dp=1) { return (bytes / Math.pow(1024, i)).toFixed(dp) * 1 + ' ' + ['b', 'kb', 'mb', 'gb', 'tb'][i]; } -function cleanHREFinput(a) { - let href_form = a; - if (a.target) { - href_form = a.target; - } - let currentTxtVal = href_form.value.trim().toLowerCase(); - //Clean the HREF to remove non lowercase letters and dashes - currentTxtVal = currentTxtVal.replace(/(?![0-9a-z\-\_])./g, ''); - href_form.value = currentTxtVal; -} - -function isValidHREF(href) { - if (href.length < 1) { - return false; - } - if (href.indexOf("/") != -1) { - return false; - } - - return true; -} - function main() { // Hide startup loading message document.getElementById("loadingscene").classList.add("hidden"); From 84fd30f357b2a205c4a95b7c96eba6183e858687 Mon Sep 17 00:00:00 2001 From: MatthewHana Date: Mon, 18 Mar 2024 06:40:14 +1100 Subject: [PATCH 4/6] Add HREF validity functions with documentation --- radicale/web/internal_data/fn.js | 34 ++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/radicale/web/internal_data/fn.js b/radicale/web/internal_data/fn.js index 85818473..a9170e35 100644 --- a/radicale/web/internal_data/fn.js +++ b/radicale/web/internal_data/fn.js @@ -1352,6 +1352,40 @@ function bytesToHumanReadable(bytes, dp=1) { return (bytes / Math.pow(1024, i)).toFixed(dp) * 1 + ' ' + ['b', 'kb', 'mb', 'gb', 'tb'][i]; } +/** + * Removed invalid HREF characters for a collection HREF. + * + * @param a A valid Input element or an onchange Event of an Input element. + */ +function cleanHREFinput(a) { + let href_form = a; + if (a.target) { + href_form = a.target; + } + let currentTxtVal = href_form.value.trim().toLowerCase(); + //Clean the HREF to remove non lowercase letters and dashes + currentTxtVal = currentTxtVal.replace(/(?![0-9a-z\-\_])./g, ''); + href_form.value = currentTxtVal; +} + +/** + * Checks if a proposed HREF for a collection has a valid format and syntax. + * + * @param href String of the porposed HREF. + * + * @return Boolean results if the HREF is valid. + */ +function isValidHREF(href) { + if (href.length < 1) { + return false; + } + if (href.indexOf("/") != -1) { + return false; + } + + return true; +} + function main() { // Hide startup loading message document.getElementById("loadingscene").classList.add("hidden"); From ffebbf1928046c8db2b3d137e90d130199a6ee37 Mon Sep 17 00:00:00 2001 From: MatthewHana Date: Mon, 18 Mar 2024 06:45:10 +1100 Subject: [PATCH 5/6] Rearrange functions to resolve false conflict. --- radicale/web/internal_data/fn.js | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/radicale/web/internal_data/fn.js b/radicale/web/internal_data/fn.js index a9170e35..207e77f8 100644 --- a/radicale/web/internal_data/fn.js +++ b/radicale/web/internal_data/fn.js @@ -1336,22 +1336,6 @@ function CreateEditCollectionScene(user, password, collection) { }; } -/** - * Format bytes to human-readable text. - * - * @param bytes Number of bytes. - * - * @return Formatted string. - */ -function bytesToHumanReadable(bytes, dp=1) { - let isNumber = !isNaN(parseFloat(bytes)) && !isNaN(bytes - 0); - if(!isNumber){ - return ""; - } - var i = bytes == 0 ? 0 : Math.floor(Math.log(bytes) / Math.log(1024)); - return (bytes / Math.pow(1024, i)).toFixed(dp) * 1 + ' ' + ['b', 'kb', 'mb', 'gb', 'tb'][i]; -} - /** * Removed invalid HREF characters for a collection HREF. * @@ -1386,6 +1370,22 @@ function isValidHREF(href) { return true; } +/** + * Format bytes to human-readable text. + * + * @param bytes Number of bytes. + * + * @return Formatted string. + */ +function bytesToHumanReadable(bytes, dp=1) { + let isNumber = !isNaN(parseFloat(bytes)) && !isNaN(bytes - 0); + if(!isNumber){ + return ""; + } + var i = bytes == 0 ? 0 : Math.floor(Math.log(bytes) / Math.log(1024)); + return (bytes / Math.pow(1024, i)).toFixed(dp) * 1 + ' ' + ['b', 'kb', 'mb', 'gb', 'tb'][i]; +} + function main() { // Hide startup loading message document.getElementById("loadingscene").classList.add("hidden"); From a49454d36e59ef40aa05de1ad20e1b6b6b72674b Mon Sep 17 00:00:00 2001 From: MatthewHana Date: Mon, 18 Mar 2024 18:59:40 +1100 Subject: [PATCH 6/6] Restore empty new line to fix conflict --- radicale/web/internal_data/fn.js | 1 + 1 file changed, 1 insertion(+) diff --git a/radicale/web/internal_data/fn.js b/radicale/web/internal_data/fn.js index 207e77f8..8c90d948 100644 --- a/radicale/web/internal_data/fn.js +++ b/radicale/web/internal_data/fn.js @@ -1386,6 +1386,7 @@ function bytesToHumanReadable(bytes, dp=1) { return (bytes / Math.pow(1024, i)).toFixed(dp) * 1 + ' ' + ['b', 'kb', 'mb', 'gb', 'tb'][i]; } + function main() { // Hide startup loading message document.getElementById("loadingscene").classList.add("hidden");