|
|
@@ -1,6 +1,6 @@
|
|
|
/**
|
|
|
* This file is part of Radicale Server - Calendar Server
|
|
|
- * Copyright © 2017-2018 Unrud <unrud@outlook.com>
|
|
|
+ * Copyright © 2017-2024 Unrud <unrud@outlook.com>
|
|
|
*
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
@@ -28,7 +28,7 @@ const SERVER = location.origin;
|
|
|
* @const
|
|
|
* @type {string}
|
|
|
*/
|
|
|
-const ROOT_PATH = (new URL("..", location.href)).pathname;
|
|
|
+const ROOT_PATH = location.pathname.replace(new RegExp("/+[^/]+/*(/index\\.html?)?$"), "") + '/';
|
|
|
|
|
|
/**
|
|
|
* Regex to match and normalize color
|
|
|
@@ -36,6 +36,13 @@ const ROOT_PATH = (new URL("..", location.href)).pathname;
|
|
|
*/
|
|
|
const COLOR_RE = new RegExp("^(#[0-9A-Fa-f]{6})(?:[0-9A-Fa-f]{2})?$");
|
|
|
|
|
|
+
|
|
|
+/**
|
|
|
+ * The text needed to confirm deleting a collection
|
|
|
+ * @const
|
|
|
+ */
|
|
|
+const DELETE_CONFIRMATION_TEXT = "DELETE";
|
|
|
+
|
|
|
/**
|
|
|
* Escape string for usage in XML
|
|
|
* @param {string} s
|
|
|
@@ -63,6 +70,7 @@ const CollectionType = {
|
|
|
CALENDAR: "CALENDAR",
|
|
|
JOURNAL: "JOURNAL",
|
|
|
TASKS: "TASKS",
|
|
|
+ WEBCAL: "WEBCAL",
|
|
|
is_subset: function(a, b) {
|
|
|
let components = a.split("_");
|
|
|
for (let i = 0; i < components.length; i++) {
|
|
|
@@ -89,7 +97,27 @@ const CollectionType = {
|
|
|
if (a.search(this.TASKS) !== -1 || b.search(this.TASKS) !== -1) {
|
|
|
union.push(this.TASKS);
|
|
|
}
|
|
|
+ if (a.search(this.WEBCAL) !== -1 || b.search(this.WEBCAL) !== -1) {
|
|
|
+ union.push(this.WEBCAL);
|
|
|
+ }
|
|
|
return union.join("_");
|
|
|
+ },
|
|
|
+ valid_options_for_type: function(a){
|
|
|
+ a = a.trim().toUpperCase();
|
|
|
+ switch(a){
|
|
|
+ case CollectionType.CALENDAR_JOURNAL_TASKS:
|
|
|
+ case CollectionType.CALENDAR_JOURNAL:
|
|
|
+ case CollectionType.CALENDAR_TASKS:
|
|
|
+ case CollectionType.JOURNAL_TASKS:
|
|
|
+ case CollectionType.CALENDAR:
|
|
|
+ case CollectionType.JOURNAL:
|
|
|
+ case CollectionType.TASKS:
|
|
|
+ return [CollectionType.CALENDAR_JOURNAL_TASKS, CollectionType.CALENDAR_JOURNAL, CollectionType.CALENDAR_TASKS, CollectionType.JOURNAL_TASKS, CollectionType.CALENDAR, CollectionType.JOURNAL, CollectionType.TASKS];
|
|
|
+ case CollectionType.ADDRESSBOOK:
|
|
|
+ case CollectionType.WEBCAL:
|
|
|
+ default:
|
|
|
+ return [a];
|
|
|
+ }
|
|
|
}
|
|
|
};
|
|
|
|
|
|
@@ -102,12 +130,15 @@ const CollectionType = {
|
|
|
* @param {string} description
|
|
|
* @param {string} color
|
|
|
*/
|
|
|
-function Collection(href, type, displayname, description, color) {
|
|
|
+function Collection(href, type, displayname, description, color, contentcount, size, source) {
|
|
|
this.href = href;
|
|
|
this.type = type;
|
|
|
this.displayname = displayname;
|
|
|
this.color = color;
|
|
|
this.description = description;
|
|
|
+ this.source = source;
|
|
|
+ this.contentcount = contentcount;
|
|
|
+ this.size = size;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -134,6 +165,7 @@ function get_principal(user, password, callback) {
|
|
|
CollectionType.PRINCIPAL,
|
|
|
displayname_element ? displayname_element.textContent : "",
|
|
|
"",
|
|
|
+ 0,
|
|
|
""), null);
|
|
|
} else {
|
|
|
callback(null, "Internal error");
|
|
|
@@ -183,6 +215,9 @@ function get_collections(user, password, collection, callback) {
|
|
|
let addressbookcolor_element = response.querySelector(response_query + " > *|propstat > *|prop > *|addressbook-color");
|
|
|
let calendardesc_element = response.querySelector(response_query + " > *|propstat > *|prop > *|calendar-description");
|
|
|
let addressbookdesc_element = response.querySelector(response_query + " > *|propstat > *|prop > *|addressbook-description");
|
|
|
+ let contentcount_element = response.querySelector(response_query + " > *|propstat > *|prop > *|getcontentcount");
|
|
|
+ let contentlength_element = response.querySelector(response_query + " > *|propstat > *|prop > *|getcontentlength");
|
|
|
+ let webcalsource_element = response.querySelector(response_query + " > *|propstat > *|prop > *|source");
|
|
|
let components_query = response_query + " > *|propstat > *|prop > *|supported-calendar-component-set";
|
|
|
let components_element = response.querySelector(components_query);
|
|
|
let href = href_element ? href_element.textContent : "";
|
|
|
@@ -190,11 +225,21 @@ function get_collections(user, password, collection, callback) {
|
|
|
let type = "";
|
|
|
let color = "";
|
|
|
let description = "";
|
|
|
+ let source = "";
|
|
|
+ let count = 0;
|
|
|
+ let size = 0;
|
|
|
if (resourcetype_element) {
|
|
|
if (resourcetype_element.querySelector(resourcetype_query + " > *|addressbook")) {
|
|
|
type = CollectionType.ADDRESSBOOK;
|
|
|
color = addressbookcolor_element ? addressbookcolor_element.textContent : "";
|
|
|
description = addressbookdesc_element ? addressbookdesc_element.textContent : "";
|
|
|
+ count = contentcount_element ? parseInt(contentcount_element.textContent) : 0;
|
|
|
+ size = contentlength_element ? parseInt(contentlength_element.textContent) : 0;
|
|
|
+ } else if (resourcetype_element.querySelector(resourcetype_query + " > *|subscribed")) {
|
|
|
+ type = CollectionType.WEBCAL;
|
|
|
+ source = webcalsource_element ? webcalsource_element.textContent : "";
|
|
|
+ color = calendarcolor_element ? calendarcolor_element.textContent : "";
|
|
|
+ description = calendardesc_element ? calendardesc_element.textContent : "";
|
|
|
} else if (resourcetype_element.querySelector(resourcetype_query + " > *|calendar")) {
|
|
|
if (components_element) {
|
|
|
if (components_element.querySelector(components_query + " > *|comp[name=VEVENT]")) {
|
|
|
@@ -209,6 +254,8 @@ function get_collections(user, password, collection, callback) {
|
|
|
}
|
|
|
color = calendarcolor_element ? calendarcolor_element.textContent : "";
|
|
|
description = calendardesc_element ? calendardesc_element.textContent : "";
|
|
|
+ count = contentcount_element ? parseInt(contentcount_element.textContent) : 0;
|
|
|
+ size = contentlength_element ? parseInt(contentlength_element.textContent) : 0;
|
|
|
}
|
|
|
}
|
|
|
let sane_color = color.trim();
|
|
|
@@ -221,7 +268,7 @@ function get_collections(user, password, collection, callback) {
|
|
|
}
|
|
|
}
|
|
|
if (href.substr(-1) === "/" && href !== collection.href && type) {
|
|
|
- collections.push(new Collection(href, type, displayname, description, sane_color));
|
|
|
+ collections.push(new Collection(href, type, displayname, description, sane_color, count, size, source));
|
|
|
}
|
|
|
}
|
|
|
collections.sort(function(a, b) {
|
|
|
@@ -235,11 +282,15 @@ function get_collections(user, password, collection, callback) {
|
|
|
}
|
|
|
};
|
|
|
request.send('<?xml version="1.0" encoding="utf-8" ?>' +
|
|
|
- '<propfind xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav" ' +
|
|
|
+ '<propfind ' +
|
|
|
+ 'xmlns="DAV:" ' +
|
|
|
+ 'xmlns:C="urn:ietf:params:xml:ns:caldav" ' +
|
|
|
'xmlns:CR="urn:ietf:params:xml:ns:carddav" ' +
|
|
|
+ 'xmlns:CS="http://calendarserver.org/ns/" ' +
|
|
|
'xmlns:I="http://apple.com/ns/ical/" ' +
|
|
|
'xmlns:INF="http://inf-it.com/ns/ab/" ' +
|
|
|
- 'xmlns:RADICALE="http://radicale.org/ns/">' +
|
|
|
+ 'xmlns:RADICALE="http://radicale.org/ns/"' +
|
|
|
+ '>' +
|
|
|
'<prop>' +
|
|
|
'<resourcetype />' +
|
|
|
'<RADICALE:displayname />' +
|
|
|
@@ -248,6 +299,9 @@ function get_collections(user, password, collection, callback) {
|
|
|
'<C:calendar-description />' +
|
|
|
'<C:supported-calendar-component-set />' +
|
|
|
'<CR:addressbook-description />' +
|
|
|
+ '<CS:source />' +
|
|
|
+ '<RADICALE:getcontentcount />' +
|
|
|
+ '<getcontentlength />' +
|
|
|
'</prop>' +
|
|
|
'</propfind>');
|
|
|
return request;
|
|
|
@@ -329,12 +383,18 @@ function create_edit_collection(user, password, collection, create, callback) {
|
|
|
let addressbook_color = "";
|
|
|
let calendar_description = "";
|
|
|
let addressbook_description = "";
|
|
|
+ let calendar_source = "";
|
|
|
let resourcetype;
|
|
|
let components = "";
|
|
|
if (collection.type === CollectionType.ADDRESSBOOK) {
|
|
|
addressbook_color = escape_xml(collection.color + (collection.color ? "ff" : ""));
|
|
|
addressbook_description = escape_xml(collection.description);
|
|
|
resourcetype = '<CR:addressbook />';
|
|
|
+ } else if (collection.type === CollectionType.WEBCAL) {
|
|
|
+ calendar_color = escape_xml(collection.color + (collection.color ? "ff" : ""));
|
|
|
+ calendar_description = escape_xml(collection.description);
|
|
|
+ resourcetype = '<CS:subscribed />';
|
|
|
+ calendar_source = collection.source;
|
|
|
} else {
|
|
|
calendar_color = escape_xml(collection.color + (collection.color ? "ff" : ""));
|
|
|
calendar_description = escape_xml(collection.description);
|
|
|
@@ -351,7 +411,7 @@ function create_edit_collection(user, password, collection, create, callback) {
|
|
|
}
|
|
|
let xml_request = create ? "mkcol" : "propertyupdate";
|
|
|
request.send('<?xml version="1.0" encoding="UTF-8" ?>' +
|
|
|
- '<' + xml_request + ' xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav" xmlns:CR="urn:ietf:params:xml:ns:carddav" xmlns:I="http://apple.com/ns/ical/" xmlns:INF="http://inf-it.com/ns/ab/">' +
|
|
|
+ '<' + xml_request + ' xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav" xmlns:CR="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:I="http://apple.com/ns/ical/" xmlns:INF="http://inf-it.com/ns/ab/">' +
|
|
|
'<set>' +
|
|
|
'<prop>' +
|
|
|
(create ? '<resourcetype><collection />' + resourcetype + '</resourcetype>' : '') +
|
|
|
@@ -361,6 +421,7 @@ function create_edit_collection(user, password, collection, create, callback) {
|
|
|
(addressbook_color ? '<INF:addressbook-color>' + addressbook_color + '</INF:addressbook-color>' : '') +
|
|
|
(addressbook_description ? '<CR:addressbook-description>' + addressbook_description + '</CR:addressbook-description>' : '') +
|
|
|
(calendar_description ? '<C:calendar-description>' + calendar_description + '</C:calendar-description>' : '') +
|
|
|
+ (calendar_source ? '<CS:source>' + calendar_source + '</CS:source>' : '') +
|
|
|
'</prop>' +
|
|
|
'</set>' +
|
|
|
(!create ? ('<remove>' +
|
|
|
@@ -481,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 = "";
|
|
|
@@ -495,7 +557,12 @@ function LoginScene() {
|
|
|
function fill_form() {
|
|
|
user_form.value = user;
|
|
|
password_form.value = "";
|
|
|
- error_form.textContent = error ? "Error: " + error : "";
|
|
|
+ if(error){
|
|
|
+ error_form.textContent = "Error: " + error;
|
|
|
+ error_form.classList.remove("hidden");
|
|
|
+ }else{
|
|
|
+ error_form.classList.add("hidden");
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
function onlogin() {
|
|
|
@@ -507,7 +574,8 @@ function LoginScene() {
|
|
|
// setup logout
|
|
|
logout_view.classList.remove("hidden");
|
|
|
logout_btn.onclick = onlogout;
|
|
|
- logout_user_form.textContent = user;
|
|
|
+ refresh_btn.onclick = refresh;
|
|
|
+ logout_user_form.textContent = user + "'s Collections";
|
|
|
// Fetch principal
|
|
|
let loading_scene = new LoadingScene();
|
|
|
push_scene(loading_scene, false);
|
|
|
@@ -557,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();
|
|
|
@@ -618,12 +694,6 @@ function CollectionsScene(user, password, collection, onerror) {
|
|
|
/** @type {?XMLHttpRequest} */ let collections_req = null;
|
|
|
/** @type {?Array<Collection>} */ let collections = null;
|
|
|
/** @type {Array<Node>} */ 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 {
|
|
|
@@ -636,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);
|
|
|
}
|
|
|
@@ -674,21 +736,24 @@ 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");
|
|
|
let title_form = node.querySelector("[data-name=title]");
|
|
|
let description_form = node.querySelector("[data-name=description]");
|
|
|
+ let contentcount_form = node.querySelector("[data-name=contentcount]");
|
|
|
let url_form = node.querySelector("[data-name=url]");
|
|
|
let color_form = node.querySelector("[data-name=color]");
|
|
|
let delete_btn = node.querySelector("[data-name=delete]");
|
|
|
let edit_btn = node.querySelector("[data-name=edit]");
|
|
|
+ let download_btn = node.querySelector("[data-name=download]");
|
|
|
if (collection.color) {
|
|
|
- color_form.style.color = collection.color;
|
|
|
- } else {
|
|
|
- color_form.classList.add("hidden");
|
|
|
+ color_form.style.background = collection.color;
|
|
|
}
|
|
|
- let possible_types = [CollectionType.ADDRESSBOOK];
|
|
|
+ let possible_types = [CollectionType.ADDRESSBOOK, CollectionType.WEBCAL];
|
|
|
[CollectionType.CALENDAR, ""].forEach(function(e) {
|
|
|
[CollectionType.union(e, CollectionType.JOURNAL), e].forEach(function(e) {
|
|
|
[CollectionType.union(e, CollectionType.TASKS), e].forEach(function(e) {
|
|
|
@@ -704,10 +769,26 @@ function CollectionsScene(user, password, collection, onerror) {
|
|
|
}
|
|
|
});
|
|
|
title_form.textContent = collection.displayname || collection.href;
|
|
|
+ if(title_form.textContent.length > 30){
|
|
|
+ title_form.classList.add("smalltext");
|
|
|
+ }
|
|
|
description_form.textContent = collection.description;
|
|
|
+ if(description_form.textContent.length > 150){
|
|
|
+ description_form.classList.add("smalltext");
|
|
|
+ }
|
|
|
+ if(collection.type != CollectionType.WEBCAL){
|
|
|
+ let contentcount_form_txt = (collection.contentcount > 0 ? Number(collection.contentcount).toLocaleString() : "No") + " item" + (collection.contentcount == 1 ? "" : "s") + " in collection";
|
|
|
+ if(collection.contentcount > 0){
|
|
|
+ contentcount_form_txt += " (" + bytesToHumanReadable(collection.size) + ")";
|
|
|
+ }
|
|
|
+ contentcount_form.textContent = contentcount_form_txt;
|
|
|
+ }
|
|
|
let href = SERVER + collection.href;
|
|
|
- url_form.href = href;
|
|
|
- url_form.textContent = href;
|
|
|
+ url_form.value = href;
|
|
|
+ download_btn.href = href;
|
|
|
+ if(collection.type == CollectionType.WEBCAL){
|
|
|
+ download_btn.parentElement.classList.add("hidden");
|
|
|
+ }
|
|
|
delete_btn.onclick = function() {return ondelete(collection);};
|
|
|
edit_btn.onclick = function() {return onedit(collection);};
|
|
|
node.classList.remove("hidden");
|
|
|
@@ -738,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 {
|
|
|
@@ -752,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) {
|
|
|
@@ -767,7 +845,6 @@ function CollectionsScene(user, password, collection, onerror) {
|
|
|
collections_req = null;
|
|
|
}
|
|
|
collections = null;
|
|
|
- filesInputForm.reset();
|
|
|
};
|
|
|
}
|
|
|
|
|
|
@@ -779,41 +856,87 @@ function CollectionsScene(user, password, collection, onerror) {
|
|
|
* @param {Collection} collection parent collection
|
|
|
* @param {Array<File>} 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<string>} */ let errors = [];
|
|
|
+ /** @type {Array<string>} */ let results = [];
|
|
|
/** @type {?Array<Node>} */ 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");
|
|
|
- }
|
|
|
+ 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[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;
|
|
|
- }
|
|
|
+ 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;
|
|
|
- errors.push(error);
|
|
|
- updateFileStatus(errors.length - 1);
|
|
|
+ results.push(result);
|
|
|
+ updateFileStatus(results.length - 1);
|
|
|
upload_next();
|
|
|
});
|
|
|
}
|
|
|
- } catch(err) {
|
|
|
+ }catch(err){
|
|
|
console.error(err);
|
|
|
}
|
|
|
- return false;
|
|
|
}
|
|
|
|
|
|
function onclose() {
|
|
|
@@ -829,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");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- this.show = function() {
|
|
|
- html_scene.classList.remove("hidden");
|
|
|
- if (errors.length < files.length) {
|
|
|
- close_btn.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;
|
|
|
}
|
|
|
- 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);
|
|
|
+ href = newhreftxtvalue;
|
|
|
+
|
|
|
+ if(uploadfile_form.files.length == 0){
|
|
|
+ alert("You must select at least one file to upload");
|
|
|
+ return false;
|
|
|
}
|
|
|
- if (scene_index === null) {
|
|
|
- scene_index = scene_stack.length - 1;
|
|
|
- upload_next();
|
|
|
+ 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");
|
|
|
+ close_btn.onclick = onclose;
|
|
|
};
|
|
|
|
|
|
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);
|
|
|
});
|
|
|
@@ -902,14 +1048,25 @@ function DeleteCollectionScene(user, password, collection) {
|
|
|
let html_scene = document.getElementById("deletecollectionscene");
|
|
|
let title_form = html_scene.querySelector("[data-name=title]");
|
|
|
let error_form = html_scene.querySelector("[data-name=error]");
|
|
|
+ let confirmation_txt = html_scene.querySelector("[data-name=confirmationtxt]");
|
|
|
+ let delete_confirmation_lbl = html_scene.querySelector("[data-name=deleteconfirmationtext]");
|
|
|
let delete_btn = html_scene.querySelector("[data-name=delete]");
|
|
|
let cancel_btn = html_scene.querySelector("[data-name=cancel]");
|
|
|
|
|
|
+ delete_confirmation_lbl.innerHTML = DELETE_CONFIRMATION_TEXT;
|
|
|
+ confirmation_txt.value = "";
|
|
|
+ confirmation_txt.addEventListener("keydown", onkeydown);
|
|
|
+
|
|
|
/** @type {?number} */ let scene_index = null;
|
|
|
/** @type {?XMLHttpRequest} */ let delete_req = null;
|
|
|
let error = "";
|
|
|
|
|
|
function ondelete() {
|
|
|
+ let confirmation_text_value = confirmation_txt.value;
|
|
|
+ if(confirmation_text_value != DELETE_CONFIRMATION_TEXT){
|
|
|
+ alert("Please type the confirmation text to delete this collection.");
|
|
|
+ return;
|
|
|
+ }
|
|
|
try {
|
|
|
let loading_scene = new LoadingScene();
|
|
|
push_scene(loading_scene);
|
|
|
@@ -940,14 +1097,27 @@ function DeleteCollectionScene(user, password, collection) {
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
+ function onkeydown(event){
|
|
|
+ if (event.keyCode !== 13) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ ondelete();
|
|
|
+ }
|
|
|
+
|
|
|
this.show = function() {
|
|
|
this.release();
|
|
|
scene_index = scene_stack.length - 1;
|
|
|
html_scene.classList.remove("hidden");
|
|
|
title_form.textContent = collection.displayname || collection.href;
|
|
|
- error_form.textContent = error ? "Error: " + error : "";
|
|
|
delete_btn.onclick = ondelete;
|
|
|
cancel_btn.onclick = oncancel;
|
|
|
+ if(error){
|
|
|
+ error_form.textContent = "Error: " + error;
|
|
|
+ error_form.classList.remove("hidden");
|
|
|
+ }else{
|
|
|
+ error_form.classList.add("hidden");
|
|
|
+ }
|
|
|
+
|
|
|
};
|
|
|
this.hide = function() {
|
|
|
html_scene.classList.add("hidden");
|
|
|
@@ -988,13 +1158,22 @@ function CreateEditCollectionScene(user, password, collection) {
|
|
|
let html_scene = document.getElementById(edit ? "editcollectionscene" : "createcollectionscene");
|
|
|
let title_form = edit ? html_scene.querySelector("[data-name=title]") : null;
|
|
|
let error_form = html_scene.querySelector("[data-name=error]");
|
|
|
+ let href_form = html_scene.querySelector("[data-name=href]");
|
|
|
+ let href_label = html_scene.querySelector("label[for=href]");
|
|
|
let displayname_form = html_scene.querySelector("[data-name=displayname]");
|
|
|
+ let displayname_label = html_scene.querySelector("label[for=displayname]");
|
|
|
let description_form = html_scene.querySelector("[data-name=description]");
|
|
|
+ let description_label = html_scene.querySelector("label[for=description]");
|
|
|
+ let source_form = html_scene.querySelector("[data-name=source]");
|
|
|
+ let source_label = html_scene.querySelector("label[for=source]");
|
|
|
let type_form = html_scene.querySelector("[data-name=type]");
|
|
|
+ let type_label = html_scene.querySelector("label[for=type]");
|
|
|
let color_form = html_scene.querySelector("[data-name=color]");
|
|
|
+ let color_label = html_scene.querySelector("label[for=color]");
|
|
|
let submit_btn = html_scene.querySelector("[data-name=submit]");
|
|
|
let cancel_btn = html_scene.querySelector("[data-name=cancel]");
|
|
|
|
|
|
+
|
|
|
/** @type {?number} */ let scene_index = null;
|
|
|
/** @type {?XMLHttpRequest} */ let create_edit_req = null;
|
|
|
let error = "";
|
|
|
@@ -1003,40 +1182,69 @@ function CreateEditCollectionScene(user, password, collection) {
|
|
|
let href = edit ? collection.href : collection.href + random_uuid() + "/";
|
|
|
let displayname = edit ? collection.displayname : "";
|
|
|
let description = edit ? collection.description : "";
|
|
|
+ let source = edit ? collection.source : "";
|
|
|
let type = edit ? collection.type : CollectionType.CALENDAR_JOURNAL_TASKS;
|
|
|
let color = edit && collection.color ? collection.color : "#" + random_hex(6);
|
|
|
|
|
|
+ if(!edit){
|
|
|
+ href_form.addEventListener("keydown", cleanHREFinput);
|
|
|
+ }
|
|
|
+
|
|
|
function remove_invalid_types() {
|
|
|
if (!edit) {
|
|
|
return;
|
|
|
}
|
|
|
/** @type {HTMLOptionsCollection} */ let options = type_form.options;
|
|
|
// remove all options that are not supersets
|
|
|
+ let valid_type_options = CollectionType.valid_options_for_type(type);
|
|
|
for (let i = options.length - 1; i >= 0; i--) {
|
|
|
- if (!CollectionType.is_subset(type, options[i].value)) {
|
|
|
+ if (valid_type_options.indexOf(options[i].value) < 0) {
|
|
|
options.remove(i);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function read_form() {
|
|
|
+ if(!edit){
|
|
|
+ cleanHREFinput(href_form);
|
|
|
+ let newhreftxtvalue = href_form.value.trim().toLowerCase();
|
|
|
+ if(!isValidHREF(newhreftxtvalue)){
|
|
|
+ alert("You must enter a valid HREF");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ href = collection.href + "/" + newhreftxtvalue + "/";
|
|
|
+ }
|
|
|
displayname = displayname_form.value;
|
|
|
description = description_form.value;
|
|
|
+ source = source_form.value;
|
|
|
type = type_form.value;
|
|
|
color = color_form.value;
|
|
|
+ return true;
|
|
|
}
|
|
|
|
|
|
function fill_form() {
|
|
|
+ if(!edit){
|
|
|
+ href_form.value = random_uuid();
|
|
|
+ }
|
|
|
displayname_form.value = displayname;
|
|
|
description_form.value = description;
|
|
|
+ source_form.value = source;
|
|
|
type_form.value = type;
|
|
|
color_form.value = color;
|
|
|
- error_form.textContent = error ? "Error: " + error : "";
|
|
|
+ if(error){
|
|
|
+ error_form.textContent = "Error: " + error;
|
|
|
+ error_form.classList.remove("hidden");
|
|
|
+ }
|
|
|
+ error_form.classList.add("hidden");
|
|
|
+ onTypeChange();
|
|
|
+ type_form.addEventListener("change", onTypeChange);
|
|
|
}
|
|
|
|
|
|
function onsubmit() {
|
|
|
try {
|
|
|
- read_form();
|
|
|
+ if(!read_form()){
|
|
|
+ return false;
|
|
|
+ }
|
|
|
let sane_color = color.trim();
|
|
|
if (sane_color) {
|
|
|
let color_match = COLOR_RE.exec(sane_color);
|
|
|
@@ -1049,7 +1257,7 @@ function CreateEditCollectionScene(user, password, collection) {
|
|
|
}
|
|
|
let loading_scene = new LoadingScene();
|
|
|
push_scene(loading_scene);
|
|
|
- let collection = new Collection(href, type, displayname, description, sane_color);
|
|
|
+ let collection = new Collection(href, type, displayname, description, sane_color, 0, 0, source);
|
|
|
let callback = function(error1) {
|
|
|
if (scene_index === null) {
|
|
|
return;
|
|
|
@@ -1082,6 +1290,17 @@ function CreateEditCollectionScene(user, password, collection) {
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
+
|
|
|
+ function onTypeChange(e){
|
|
|
+ if(type_form.value == CollectionType.WEBCAL){
|
|
|
+ source_label.classList.remove("hidden");
|
|
|
+ source_form.classList.remove("hidden");
|
|
|
+ }else{
|
|
|
+ source_label.classList.add("hidden");
|
|
|
+ source_form.classList.add("hidden");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
this.show = function() {
|
|
|
this.release();
|
|
|
scene_index = scene_stack.length - 1;
|
|
|
@@ -1117,6 +1336,57 @@ function CreateEditCollectionScene(user, password, collection) {
|
|
|
};
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * 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;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 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");
|