mirror of
https://github.com/Kozea/Radicale.git
synced 2025-08-01 18:18:31 +00:00
[WEB UI] New WebUI Improvements
Added WebUI improvements as discussed in discussion #1416 in March 2024.
This commit is contained in:
parent
825464f102
commit
ed6432706f
3 changed files with 114 additions and 6 deletions
|
@ -196,6 +196,11 @@ main{
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#collectionsscene article small[data-name=contentcount]{
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
#editcollectionscene p span{
|
#editcollectionscene p span{
|
||||||
word-wrap:break-word;
|
word-wrap:break-word;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
@ -228,6 +233,12 @@ main{
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.deleteconfirmationtxt{
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
.fabcontainer{
|
.fabcontainer{
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
|
|
|
@ -36,6 +36,13 @@ const ROOT_PATH = location.pathname.replace(new RegExp("/+[^/]+/*(/index\\.html?
|
||||||
*/
|
*/
|
||||||
const COLOR_RE = new RegExp("^(#[0-9A-Fa-f]{6})(?:[0-9A-Fa-f]{2})?$");
|
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
|
* Escape string for usage in XML
|
||||||
* @param {string} s
|
* @param {string} s
|
||||||
|
@ -94,6 +101,23 @@ const CollectionType = {
|
||||||
union.push(this.WEBCAL);
|
union.push(this.WEBCAL);
|
||||||
}
|
}
|
||||||
return union.join("_");
|
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];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -106,13 +130,14 @@ const CollectionType = {
|
||||||
* @param {string} description
|
* @param {string} description
|
||||||
* @param {string} color
|
* @param {string} color
|
||||||
*/
|
*/
|
||||||
function Collection(href, type, displayname, description, color, source) {
|
function Collection(href, type, displayname, description, color, contentcount, source) {
|
||||||
this.href = href;
|
this.href = href;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.displayname = displayname;
|
this.displayname = displayname;
|
||||||
this.color = color;
|
this.color = color;
|
||||||
this.description = description;
|
this.description = description;
|
||||||
this.source = source;
|
this.source = source;
|
||||||
|
this.contentcount = contentcount;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -139,6 +164,7 @@ function get_principal(user, password, callback) {
|
||||||
CollectionType.PRINCIPAL,
|
CollectionType.PRINCIPAL,
|
||||||
displayname_element ? displayname_element.textContent : "",
|
displayname_element ? displayname_element.textContent : "",
|
||||||
"",
|
"",
|
||||||
|
0,
|
||||||
""), null);
|
""), null);
|
||||||
} else {
|
} else {
|
||||||
callback(null, "Internal error");
|
callback(null, "Internal error");
|
||||||
|
@ -188,6 +214,7 @@ function get_collections(user, password, collection, callback) {
|
||||||
let addressbookcolor_element = response.querySelector(response_query + " > *|propstat > *|prop > *|addressbook-color");
|
let addressbookcolor_element = response.querySelector(response_query + " > *|propstat > *|prop > *|addressbook-color");
|
||||||
let calendardesc_element = response.querySelector(response_query + " > *|propstat > *|prop > *|calendar-description");
|
let calendardesc_element = response.querySelector(response_query + " > *|propstat > *|prop > *|calendar-description");
|
||||||
let addressbookdesc_element = response.querySelector(response_query + " > *|propstat > *|prop > *|addressbook-description");
|
let addressbookdesc_element = response.querySelector(response_query + " > *|propstat > *|prop > *|addressbook-description");
|
||||||
|
let contentcount_element = response.querySelector(response_query + " > *|propstat > *|prop > *|getcontentcount");
|
||||||
let webcalsource_element = response.querySelector(response_query + " > *|propstat > *|prop > *|source");
|
let webcalsource_element = response.querySelector(response_query + " > *|propstat > *|prop > *|source");
|
||||||
let components_query = response_query + " > *|propstat > *|prop > *|supported-calendar-component-set";
|
let components_query = response_query + " > *|propstat > *|prop > *|supported-calendar-component-set";
|
||||||
let components_element = response.querySelector(components_query);
|
let components_element = response.querySelector(components_query);
|
||||||
|
@ -197,11 +224,13 @@ function get_collections(user, password, collection, callback) {
|
||||||
let color = "";
|
let color = "";
|
||||||
let description = "";
|
let description = "";
|
||||||
let source = "";
|
let source = "";
|
||||||
|
let count = 0;
|
||||||
if (resourcetype_element) {
|
if (resourcetype_element) {
|
||||||
if (resourcetype_element.querySelector(resourcetype_query + " > *|addressbook")) {
|
if (resourcetype_element.querySelector(resourcetype_query + " > *|addressbook")) {
|
||||||
type = CollectionType.ADDRESSBOOK;
|
type = CollectionType.ADDRESSBOOK;
|
||||||
color = addressbookcolor_element ? addressbookcolor_element.textContent : "";
|
color = addressbookcolor_element ? addressbookcolor_element.textContent : "";
|
||||||
description = addressbookdesc_element ? addressbookdesc_element.textContent : "";
|
description = addressbookdesc_element ? addressbookdesc_element.textContent : "";
|
||||||
|
count = contentcount_element ? parseInt(contentcount_element.textContent) : 0;
|
||||||
} else if (resourcetype_element.querySelector(resourcetype_query + " > *|subscribed")) {
|
} else if (resourcetype_element.querySelector(resourcetype_query + " > *|subscribed")) {
|
||||||
type = CollectionType.WEBCAL;
|
type = CollectionType.WEBCAL;
|
||||||
source = webcalsource_element ? webcalsource_element.textContent : "";
|
source = webcalsource_element ? webcalsource_element.textContent : "";
|
||||||
|
@ -221,6 +250,7 @@ function get_collections(user, password, collection, callback) {
|
||||||
}
|
}
|
||||||
color = calendarcolor_element ? calendarcolor_element.textContent : "";
|
color = calendarcolor_element ? calendarcolor_element.textContent : "";
|
||||||
description = calendardesc_element ? calendardesc_element.textContent : "";
|
description = calendardesc_element ? calendardesc_element.textContent : "";
|
||||||
|
count = contentcount_element ? parseInt(contentcount_element.textContent) : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let sane_color = color.trim();
|
let sane_color = color.trim();
|
||||||
|
@ -233,7 +263,7 @@ function get_collections(user, password, collection, callback) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (href.substr(-1) === "/" && href !== collection.href && type) {
|
if (href.substr(-1) === "/" && href !== collection.href && type) {
|
||||||
collections.push(new Collection(href, type, displayname, description, sane_color, source));
|
collections.push(new Collection(href, type, displayname, description, sane_color, count, source));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
collections.sort(function(a, b) {
|
collections.sort(function(a, b) {
|
||||||
|
@ -265,6 +295,7 @@ function get_collections(user, password, collection, callback) {
|
||||||
'<C:supported-calendar-component-set />' +
|
'<C:supported-calendar-component-set />' +
|
||||||
'<CR:addressbook-description />' +
|
'<CR:addressbook-description />' +
|
||||||
'<CS:source />' +
|
'<CS:source />' +
|
||||||
|
'<RADICALE:getcontentcount />' +
|
||||||
'</prop>' +
|
'</prop>' +
|
||||||
'</propfind>');
|
'</propfind>');
|
||||||
return request;
|
return request;
|
||||||
|
@ -708,6 +739,7 @@ function CollectionsScene(user, password, collection, onerror) {
|
||||||
node.classList.remove("hidden");
|
node.classList.remove("hidden");
|
||||||
let title_form = node.querySelector("[data-name=title]");
|
let title_form = node.querySelector("[data-name=title]");
|
||||||
let description_form = node.querySelector("[data-name=description]");
|
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 url_form = node.querySelector("[data-name=url]");
|
||||||
let color_form = node.querySelector("[data-name=color]");
|
let color_form = node.querySelector("[data-name=color]");
|
||||||
let delete_btn = node.querySelector("[data-name=delete]");
|
let delete_btn = node.querySelector("[data-name=delete]");
|
||||||
|
@ -739,6 +771,9 @@ function CollectionsScene(user, password, collection, onerror) {
|
||||||
if(description_form.textContent.length > 150){
|
if(description_form.textContent.length > 150){
|
||||||
description_form.classList.add("smalltext");
|
description_form.classList.add("smalltext");
|
||||||
}
|
}
|
||||||
|
if(collection.type != CollectionType.WEBCAL){
|
||||||
|
contentcount_form.textContent = (collection.contentcount > 0 ? collection.contentcount : "No") + " item" + (collection.contentcount == 1 ? "" : "s") + " in collection";
|
||||||
|
}
|
||||||
let href = SERVER + collection.href;
|
let href = SERVER + collection.href;
|
||||||
url_form.value = href;
|
url_form.value = href;
|
||||||
download_btn.href = href;
|
download_btn.href = href;
|
||||||
|
@ -939,14 +974,25 @@ function DeleteCollectionScene(user, password, collection) {
|
||||||
let html_scene = document.getElementById("deletecollectionscene");
|
let html_scene = document.getElementById("deletecollectionscene");
|
||||||
let title_form = html_scene.querySelector("[data-name=title]");
|
let title_form = html_scene.querySelector("[data-name=title]");
|
||||||
let error_form = html_scene.querySelector("[data-name=error]");
|
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 delete_btn = html_scene.querySelector("[data-name=delete]");
|
||||||
let cancel_btn = html_scene.querySelector("[data-name=cancel]");
|
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 {?number} */ let scene_index = null;
|
||||||
/** @type {?XMLHttpRequest} */ let delete_req = null;
|
/** @type {?XMLHttpRequest} */ let delete_req = null;
|
||||||
let error = "";
|
let error = "";
|
||||||
|
|
||||||
function ondelete() {
|
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 {
|
try {
|
||||||
let loading_scene = new LoadingScene();
|
let loading_scene = new LoadingScene();
|
||||||
push_scene(loading_scene);
|
push_scene(loading_scene);
|
||||||
|
@ -977,6 +1023,13 @@ function DeleteCollectionScene(user, password, collection) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onkeydown(event){
|
||||||
|
if (event.keyCode !== 13) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ondelete();
|
||||||
|
}
|
||||||
|
|
||||||
this.show = function() {
|
this.show = function() {
|
||||||
this.release();
|
this.release();
|
||||||
scene_index = scene_stack.length - 1;
|
scene_index = scene_stack.length - 1;
|
||||||
|
@ -1031,6 +1084,8 @@ function CreateEditCollectionScene(user, password, collection) {
|
||||||
let html_scene = document.getElementById(edit ? "editcollectionscene" : "createcollectionscene");
|
let html_scene = document.getElementById(edit ? "editcollectionscene" : "createcollectionscene");
|
||||||
let title_form = edit ? html_scene.querySelector("[data-name=title]") : null;
|
let title_form = edit ? html_scene.querySelector("[data-name=title]") : null;
|
||||||
let error_form = html_scene.querySelector("[data-name=error]");
|
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_form = html_scene.querySelector("[data-name=displayname]");
|
||||||
let displayname_label = html_scene.querySelector("label[for=displayname]");
|
let displayname_label = html_scene.querySelector("label[for=displayname]");
|
||||||
let description_form = html_scene.querySelector("[data-name=description]");
|
let description_form = html_scene.querySelector("[data-name=description]");
|
||||||
|
@ -1057,28 +1112,46 @@ function CreateEditCollectionScene(user, password, collection) {
|
||||||
let type = edit ? collection.type : CollectionType.CALENDAR_JOURNAL_TASKS;
|
let type = edit ? collection.type : CollectionType.CALENDAR_JOURNAL_TASKS;
|
||||||
let color = edit && collection.color ? collection.color : "#" + random_hex(6);
|
let color = edit && collection.color ? collection.color : "#" + random_hex(6);
|
||||||
|
|
||||||
|
if(!edit){
|
||||||
|
href_form.addEventListener("keydown", cleanHREFinput);
|
||||||
|
}
|
||||||
|
|
||||||
function remove_invalid_types() {
|
function remove_invalid_types() {
|
||||||
if (!edit) {
|
if (!edit) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
/** @type {HTMLOptionsCollection} */ let options = type_form.options;
|
/** @type {HTMLOptionsCollection} */ let options = type_form.options;
|
||||||
// remove all options that are not supersets
|
// 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--) {
|
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);
|
options.remove(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function read_form() {
|
function read_form() {
|
||||||
|
if(!edit){
|
||||||
|
cleanHREFinput();
|
||||||
|
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;
|
displayname = displayname_form.value;
|
||||||
description = description_form.value;
|
description = description_form.value;
|
||||||
source = source_form.value;
|
source = source_form.value;
|
||||||
type = type_form.value;
|
type = type_form.value;
|
||||||
color = color_form.value;
|
color = color_form.value;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function fill_form() {
|
function fill_form() {
|
||||||
|
if(!edit){
|
||||||
|
href_form.value = random_uuid();
|
||||||
|
}
|
||||||
displayname_form.value = displayname;
|
displayname_form.value = displayname;
|
||||||
description_form.value = description;
|
description_form.value = description;
|
||||||
source_form.value = source;
|
source_form.value = source;
|
||||||
|
@ -1095,7 +1168,9 @@ function CreateEditCollectionScene(user, password, collection) {
|
||||||
|
|
||||||
function onsubmit() {
|
function onsubmit() {
|
||||||
try {
|
try {
|
||||||
read_form();
|
if(!read_form()){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
let sane_color = color.trim();
|
let sane_color = color.trim();
|
||||||
if (sane_color) {
|
if (sane_color) {
|
||||||
let color_match = COLOR_RE.exec(sane_color);
|
let color_match = COLOR_RE.exec(sane_color);
|
||||||
|
@ -1108,7 +1183,7 @@ function CreateEditCollectionScene(user, password, collection) {
|
||||||
}
|
}
|
||||||
let loading_scene = new LoadingScene();
|
let loading_scene = new LoadingScene();
|
||||||
push_scene(loading_scene);
|
push_scene(loading_scene);
|
||||||
let collection = new Collection(href, type, displayname, description, sane_color, source);
|
let collection = new Collection(href, type, displayname, description, sane_color, 0, source);
|
||||||
let callback = function(error1) {
|
let callback = function(error1) {
|
||||||
if (scene_index === null) {
|
if (scene_index === null) {
|
||||||
return;
|
return;
|
||||||
|
@ -1141,6 +1216,13 @@ function CreateEditCollectionScene(user, password, collection) {
|
||||||
return false;
|
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){
|
function onTypeChange(e){
|
||||||
if(type_form.value == CollectionType.WEBCAL){
|
if(type_form.value == CollectionType.WEBCAL){
|
||||||
source_label.classList.remove("hidden");
|
source_label.classList.remove("hidden");
|
||||||
|
@ -1151,6 +1233,17 @@ 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.show = function() {
|
||||||
this.release();
|
this.release();
|
||||||
scene_index = scene_stack.length - 1;
|
scene_index = scene_stack.length - 1;
|
||||||
|
|
|
@ -62,6 +62,7 @@
|
||||||
<span data-name="TASKS">Tasks</span>
|
<span data-name="TASKS">Tasks</span>
|
||||||
<span data-name="WEBCAL">Webcal</span>
|
<span data-name="WEBCAL">Webcal</span>
|
||||||
</small>
|
</small>
|
||||||
|
<small data-name="contentcount"></small>
|
||||||
<input type="text" data-name="url" value="" readonly="" onfocus="this.setSelectionRange(0, 99999);">
|
<input type="text" data-name="url" value="" readonly="" onfocus="this.setSelectionRange(0, 99999);">
|
||||||
<p data-name="description" style="word-wrap:break-word;">Description</p>
|
<p data-name="description" style="word-wrap:break-word;">Description</p>
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -131,6 +132,8 @@
|
||||||
<option value="TASKS">Tasks</option>
|
<option value="TASKS">Tasks</option>
|
||||||
<option value="WEBCAL">Webcal</option>
|
<option value="WEBCAL">Webcal</option>
|
||||||
</select>
|
</select>
|
||||||
|
<label for="href">HREF:</label>
|
||||||
|
<input data-name="href" type="text">
|
||||||
<label for="displayname">Title:</label>
|
<label for="displayname">Title:</label>
|
||||||
<input data-name="displayname" type="text">
|
<input data-name="displayname" type="text">
|
||||||
<label for="description">Description:</label>
|
<label for="description">Description:</label>
|
||||||
|
@ -164,7 +167,8 @@
|
||||||
|
|
||||||
<section id="deletecollectionscene" class="container hidden">
|
<section id="deletecollectionscene" class="container hidden">
|
||||||
<h1>Delete Collection</h1>
|
<h1>Delete Collection</h1>
|
||||||
<p>Do you want to delete the collection <span class="title" data-name="title">title</span>? </p>
|
<p>To delete the collection <span class="title" data-name="title">title</span> please enter the phrase <strong data-name="deleteconfirmationtext"></strong> in the box below:</p>
|
||||||
|
<input type="text" class="deleteconfirmationtxt" data-name="confirmationtxt" />
|
||||||
<p class="red">WARNING: This action cannot be reversed.</p>
|
<p class="red">WARNING: This action cannot be reversed.</p>
|
||||||
<form>
|
<form>
|
||||||
<button type="button" class="red" data-name="delete">Delete</button>
|
<button type="button" class="red" data-name="delete">Delete</button>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue