2017-05-31 13:18:42 +02:00
/ * *
* This file is part of Radicale Server - Calendar Server
2024-03-05 23:57:58 +11:00
* Copyright © 2017 - 2024 Unrud < unrud @ outlook . com >
2024-10-08 08:05:52 +02:00
* Copyright © 2023 - 2024 Matthew Hana < matthew . hana @ gmail . com >
2025-02-27 19:40:11 +01:00
* Copyright © 2024 - 2025 Peter Bieringer < pb @ bieringer . de >
2017-05-31 13:18:42 +02:00
*
* 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
* the Free Software Foundation , either version 3 of the License , or
* ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU General Public License for more details .
*
* You should have received a copy of the GNU General Public License
* along with this program . If not , see < http : //www.gnu.org/licenses/>.
* /
/ * *
2017-06-01 12:04:25 +02:00
* Server address
2017-05-31 13:18:42 +02:00
* @ const
* @ type { string }
* /
2022-01-07 23:23:53 +01:00
const SERVER = location . origin ;
2017-06-01 12:04:25 +02:00
/ * *
* Path of the root collection on the server ( must end with / )
* @ const
* @ type { string }
* /
2023-08-31 20:42:41 +10:00
const ROOT _PATH = location . pathname . replace ( new RegExp ( "/+[^/]+/*(/index\\.html?)?$" ) , "" ) + '/' ;
2017-05-31 13:18:42 +02:00
/ * *
* Regex to match and normalize color
* @ const
* /
2020-01-16 03:58:29 +01:00
const COLOR _RE = new RegExp ( "^(#[0-9A-Fa-f]{6})(?:[0-9A-Fa-f]{2})?$" ) ;
2017-05-31 13:18:42 +02:00
2024-03-13 07:42:47 +11:00
/ * *
* The text needed to confirm deleting a collection
* @ const
* /
const DELETE _CONFIRMATION _TEXT = "DELETE" ;
2017-05-31 13:18:42 +02:00
/ * *
* Escape string for usage in XML
* @ param { string } s
* @ return { string }
* /
function escape _xml ( s ) {
return ( s
2020-04-28 14:11:35 +02:00
. replace ( /&/g , "&" )
. replace ( /"/g , """ )
. replace ( /'/g , "'" )
. replace ( /</g , "<" )
. replace ( />/g , ">" ) ) ;
2017-05-31 13:18:42 +02:00
}
/ * *
* @ enum { string }
* /
2020-01-16 03:58:29 +01:00
const CollectionType = {
2017-05-31 13:18:42 +02:00
PRINCIPAL : "PRINCIPAL" ,
ADDRESSBOOK : "ADDRESSBOOK" ,
CALENDAR _JOURNAL _TASKS : "CALENDAR_JOURNAL_TASKS" ,
CALENDAR _JOURNAL : "CALENDAR_JOURNAL" ,
CALENDAR _TASKS : "CALENDAR_TASKS" ,
JOURNAL _TASKS : "JOURNAL_TASKS" ,
CALENDAR : "CALENDAR" ,
JOURNAL : "JOURNAL" ,
TASKS : "TASKS" ,
2024-03-05 23:57:58 +11:00
WEBCAL : "WEBCAL" ,
2017-05-31 13:18:42 +02:00
is _subset : function ( a , b ) {
2020-01-16 03:58:29 +01:00
let components = a . split ( "_" ) ;
for ( let i = 0 ; i < components . length ; i ++ ) {
2017-05-31 13:18:42 +02:00
if ( b . search ( components [ i ] ) === - 1 ) {
return false ;
}
}
return true ;
} ,
union : function ( a , b ) {
if ( a . search ( this . ADDRESSBOOK ) !== - 1 || b . search ( this . ADDRESSBOOK ) !== - 1 ) {
if ( a && a !== this . ADDRESSBOOK || b && b !== this . ADDRESSBOOK ) {
throw "Invalid union: " + a + " " + b ;
}
return this . ADDRESSBOOK ;
}
2020-04-28 21:56:13 +02:00
let union = [ ] ;
2017-05-31 13:18:42 +02:00
if ( a . search ( this . CALENDAR ) !== - 1 || b . search ( this . CALENDAR ) !== - 1 ) {
2020-04-28 21:56:13 +02:00
union . push ( this . CALENDAR ) ;
2017-05-31 13:18:42 +02:00
}
if ( a . search ( this . JOURNAL ) !== - 1 || b . search ( this . JOURNAL ) !== - 1 ) {
2020-04-28 21:56:13 +02:00
union . push ( this . JOURNAL ) ;
2017-05-31 13:18:42 +02:00
}
if ( a . search ( this . TASKS ) !== - 1 || b . search ( this . TASKS ) !== - 1 ) {
2020-04-28 21:56:13 +02:00
union . push ( this . TASKS ) ;
2017-05-31 13:18:42 +02:00
}
2024-03-05 23:57:58 +11:00
if ( a . search ( this . WEBCAL ) !== - 1 || b . search ( this . WEBCAL ) !== - 1 ) {
union . push ( this . WEBCAL ) ;
}
2020-04-28 21:56:13 +02:00
return union . join ( "_" ) ;
2024-03-13 07:42:47 +11:00
} ,
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 ] ;
}
2017-05-31 13:18:42 +02:00
}
} ;
/ * *
* @ constructor
* @ struct
* @ param { string } href Must always start and end with / .
* @ param { CollectionType } type
* @ param { string } displayname
* @ param { string } description
* @ param { string } color
* /
2024-03-13 05:07:22 +11:00
function Collection ( href , type , displayname , description , color , contentcount , size , source ) {
2017-05-31 13:18:42 +02:00
this . href = href ;
this . type = type ;
this . displayname = displayname ;
this . color = color ;
this . description = description ;
2024-03-05 23:57:58 +11:00
this . source = source ;
2024-03-13 07:42:47 +11:00
this . contentcount = contentcount ;
2024-03-13 05:07:22 +11:00
this . size = size ;
2017-05-31 13:18:42 +02:00
}
/ * *
* Find the principal collection .
* @ param { string } user
* @ param { string } password
* @ param { function ( ? Collection , ? string ) } callback Returns result or error
* @ return { XMLHttpRequest }
* /
function get _principal ( user , password , callback ) {
2020-01-16 03:58:29 +01:00
let request = new XMLHttpRequest ( ) ;
2021-01-08 10:40:37 +01:00
request . open ( "PROPFIND" , SERVER + ROOT _PATH , true , user , encodeURIComponent ( password ) ) ;
2017-05-31 13:18:42 +02:00
request . onreadystatechange = function ( ) {
if ( request . readyState !== 4 ) {
return ;
}
if ( request . status === 207 ) {
2020-01-16 03:58:29 +01:00
let xml = request . responseXML ;
let principal _element = xml . querySelector ( "*|multistatus:root > *|response:first-of-type > *|propstat > *|prop > *|current-user-principal > *|href" ) ;
let displayname _element = xml . querySelector ( "*|multistatus:root > *|response:first-of-type > *|propstat > *|prop > *|displayname" ) ;
2017-05-31 13:18:42 +02:00
if ( principal _element ) {
callback ( new Collection (
principal _element . textContent ,
CollectionType . PRINCIPAL ,
displayname _element ? displayname _element . textContent : "" ,
"" ,
2024-03-13 07:42:47 +11:00
0 ,
2017-05-31 13:18:42 +02:00
"" ) , null ) ;
} else {
callback ( null , "Internal error" ) ;
}
} else {
callback ( null , request . status + " " + request . statusText ) ;
}
} ;
request . send ( '<?xml version="1.0" encoding="utf-8" ?>' +
'<propfind xmlns="DAV:">' +
'<prop>' +
'<current-user-principal />' +
'<displayname />' +
'</prop>' +
'</propfind>' ) ;
return request ;
}
/ * *
* Find all calendars and addressbooks in collection .
* @ param { string } user
* @ param { string } password
* @ param { Collection } collection
* @ param { function ( ? Array < Collection > , ? string ) } callback Returns result or error
* @ return { XMLHttpRequest }
* /
function get _collections ( user , password , collection , callback ) {
2020-01-16 03:58:29 +01:00
let request = new XMLHttpRequest ( ) ;
2021-01-08 10:40:37 +01:00
request . open ( "PROPFIND" , SERVER + collection . href , true , user , encodeURIComponent ( password ) ) ;
2017-05-31 13:18:42 +02:00
request . setRequestHeader ( "depth" , "1" ) ;
request . onreadystatechange = function ( ) {
if ( request . readyState !== 4 ) {
return ;
}
if ( request . status === 207 ) {
2020-01-16 03:58:29 +01:00
let xml = request . responseXML ;
let collections = [ ] ;
let response _query = "*|multistatus:root > *|response" ;
let responses = xml . querySelectorAll ( response _query ) ;
for ( let i = 0 ; i < responses . length ; i ++ ) {
let response = responses [ i ] ;
let href _element = response . querySelector ( response _query + " > *|href" ) ;
let resourcetype _query = response _query + " > *|propstat > *|prop > *|resourcetype" ;
let resourcetype _element = response . querySelector ( resourcetype _query ) ;
let displayname _element = response . querySelector ( response _query + " > *|propstat > *|prop > *|displayname" ) ;
let calendarcolor _element = response . querySelector ( response _query + " > *|propstat > *|prop > *|calendar-color" ) ;
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" ) ;
2024-03-13 07:42:47 +11:00
let contentcount _element = response . querySelector ( response _query + " > *|propstat > *|prop > *|getcontentcount" ) ;
2024-03-13 05:07:22 +11:00
let contentlength _element = response . querySelector ( response _query + " > *|propstat > *|prop > *|getcontentlength" ) ;
2024-03-05 23:57:58 +11:00
let webcalsource _element = response . querySelector ( response _query + " > *|propstat > *|prop > *|source" ) ;
2020-01-16 03:58:29 +01:00
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 : "" ;
let displayname = displayname _element ? displayname _element . textContent : "" ;
let type = "" ;
let color = "" ;
let description = "" ;
2024-03-05 23:57:58 +11:00
let source = "" ;
2024-03-13 07:42:47 +11:00
let count = 0 ;
2024-03-13 05:07:22 +11:00
let size = 0 ;
2017-05-31 13:18:42 +02:00
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 : "" ;
2024-03-13 07:42:47 +11:00
count = contentcount _element ? parseInt ( contentcount _element . textContent ) : 0 ;
2024-03-13 05:07:22 +11:00
size = contentlength _element ? parseInt ( contentlength _element . textContent ) : 0 ;
2024-03-05 23:57:58 +11:00
} else if ( resourcetype _element . querySelector ( resourcetype _query + " > *|subscribed" ) ) {
2024-03-06 01:50:26 +11:00
type = CollectionType . WEBCAL ;
2024-03-05 23:57:58 +11:00
source = webcalsource _element ? webcalsource _element . textContent : "" ;
color = calendarcolor _element ? calendarcolor _element . textContent : "" ;
description = calendardesc _element ? calendardesc _element . textContent : "" ;
2017-05-31 13:18:42 +02:00
} else if ( resourcetype _element . querySelector ( resourcetype _query + " > *|calendar" ) ) {
if ( components _element ) {
if ( components _element . querySelector ( components _query + " > *|comp[name=VEVENT]" ) ) {
type = CollectionType . union ( type , CollectionType . CALENDAR ) ;
}
if ( components _element . querySelector ( components _query + " > *|comp[name=VJOURNAL]" ) ) {
type = CollectionType . union ( type , CollectionType . JOURNAL ) ;
}
if ( components _element . querySelector ( components _query + " > *|comp[name=VTODO]" ) ) {
type = CollectionType . union ( type , CollectionType . TASKS ) ;
}
}
color = calendarcolor _element ? calendarcolor _element . textContent : "" ;
description = calendardesc _element ? calendardesc _element . textContent : "" ;
2024-03-13 07:42:47 +11:00
count = contentcount _element ? parseInt ( contentcount _element . textContent ) : 0 ;
2024-03-13 05:07:22 +11:00
size = contentlength _element ? parseInt ( contentlength _element . textContent ) : 0 ;
2017-05-31 13:18:42 +02:00
}
}
2020-01-16 03:58:29 +01:00
let sane _color = color . trim ( ) ;
2017-05-31 13:18:42 +02:00
if ( sane _color ) {
2020-01-16 03:58:29 +01:00
let color _match = COLOR _RE . exec ( sane _color ) ;
2017-05-31 13:18:42 +02:00
if ( color _match ) {
sane _color = color _match [ 1 ] ;
} else {
sane _color = "" ;
}
}
if ( href . substr ( - 1 ) === "/" && href !== collection . href && type ) {
2024-03-13 05:07:22 +11:00
collections . push ( new Collection ( href , type , displayname , description , sane _color , count , size , source ) ) ;
2017-05-31 13:18:42 +02:00
}
}
collections . sort ( function ( a , b ) {
2020-01-16 03:58:29 +01:00
/** @type {string} */ let ca = a . displayname || a . href ;
/** @type {string} */ let cb = b . displayname || b . href ;
2017-05-31 13:18:42 +02:00
return ca . localeCompare ( cb ) ;
} ) ;
callback ( collections , null ) ;
} else {
callback ( null , request . status + " " + request . statusText ) ;
}
} ;
request . send ( '<?xml version="1.0" encoding="utf-8" ?>' +
2024-03-09 08:06:04 +01:00
'<propfind ' +
'xmlns="DAV:" ' +
2024-03-05 23:57:58 +11:00
'xmlns:C="urn:ietf:params:xml:ns:caldav" ' +
2017-09-17 14:03:50 +02:00
'xmlns:CR="urn:ietf:params:xml:ns:carddav" ' +
2024-03-05 23:57:58 +11:00
'xmlns:CS="http://calendarserver.org/ns/" ' +
2017-09-17 14:03:50 +02:00
'xmlns:I="http://apple.com/ns/ical/" ' +
'xmlns:INF="http://inf-it.com/ns/ab/" ' +
2024-03-09 08:06:04 +01:00
'xmlns:RADICALE="http://radicale.org/ns/"' +
2024-03-05 23:57:58 +11:00
'>' +
2017-05-31 13:18:42 +02:00
'<prop>' +
'<resourcetype />' +
2017-09-17 14:03:50 +02:00
'<RADICALE:displayname />' +
2017-05-31 13:18:42 +02:00
'<I:calendar-color />' +
'<INF:addressbook-color />' +
'<C:calendar-description />' +
'<C:supported-calendar-component-set />' +
'<CR:addressbook-description />' +
2024-03-05 23:57:58 +11:00
'<CS:source />' +
2024-03-13 07:42:47 +11:00
'<RADICALE:getcontentcount />' +
2024-03-13 05:07:22 +11:00
'<getcontentlength />' +
2017-05-31 13:18:42 +02:00
'</prop>' +
'</propfind>' ) ;
return request ;
}
2018-08-18 12:56:42 +02:00
/ * *
* @ param { string } user
* @ param { string } password
* @ param { string } collection _href Must always start and end with / .
* @ param { File } file
* @ param { function ( ? string ) } callback Returns error or null
* @ return { XMLHttpRequest }
* /
function upload _collection ( user , password , collection _href , file , callback ) {
2020-01-16 03:58:29 +01:00
let request = new XMLHttpRequest ( ) ;
2021-01-08 10:40:37 +01:00
request . open ( "PUT" , SERVER + collection _href , true , user , encodeURIComponent ( password ) ) ;
2018-08-18 12:56:42 +02:00
request . onreadystatechange = function ( ) {
if ( request . readyState !== 4 ) {
return ;
}
if ( 200 <= request . status && request . status < 300 ) {
callback ( null ) ;
} else {
callback ( request . status + " " + request . statusText ) ;
}
} ;
2018-08-18 14:08:02 +02:00
request . setRequestHeader ( "If-None-Match" , "*" ) ;
2018-08-18 12:56:42 +02:00
request . send ( file ) ;
return request ;
}
2017-05-31 13:18:42 +02:00
/ * *
* @ param { string } user
* @ param { string } password
* @ param { Collection } collection
* @ param { function ( ? string ) } callback Returns error or null
* @ return { XMLHttpRequest }
* /
function delete _collection ( user , password , collection , callback ) {
2020-01-16 03:58:29 +01:00
let request = new XMLHttpRequest ( ) ;
2021-01-08 10:40:37 +01:00
request . open ( "DELETE" , SERVER + collection . href , true , user , encodeURIComponent ( password ) ) ;
2017-05-31 13:18:42 +02:00
request . onreadystatechange = function ( ) {
if ( request . readyState !== 4 ) {
return ;
}
if ( 200 <= request . status && request . status < 300 ) {
callback ( null ) ;
} else {
callback ( request . status + " " + request . statusText ) ;
}
} ;
request . send ( ) ;
return request ;
}
/ * *
* @ param { string } user
* @ param { string } password
* @ param { Collection } collection
* @ param { boolean } create
* @ param { function ( ? string ) } callback Returns error or null
* @ return { XMLHttpRequest }
* /
function create _edit _collection ( user , password , collection , create , callback ) {
2020-01-16 03:58:29 +01:00
let request = new XMLHttpRequest ( ) ;
2021-01-08 10:40:37 +01:00
request . open ( create ? "MKCOL" : "PROPPATCH" , SERVER + collection . href , true , user , encodeURIComponent ( password ) ) ;
2017-05-31 13:18:42 +02:00
request . onreadystatechange = function ( ) {
if ( request . readyState !== 4 ) {
return ;
}
if ( 200 <= request . status && request . status < 300 ) {
callback ( null ) ;
} else {
callback ( request . status + " " + request . statusText ) ;
}
} ;
2020-01-16 03:58:29 +01:00
let displayname = escape _xml ( collection . displayname ) ;
let calendar _color = "" ;
let addressbook _color = "" ;
let calendar _description = "" ;
let addressbook _description = "" ;
2024-03-05 23:57:58 +11:00
let calendar _source = "" ;
2020-01-16 03:58:29 +01:00
let resourcetype ;
let components = "" ;
2017-05-31 13:18:42 +02:00
if ( collection . type === CollectionType . ADDRESSBOOK ) {
addressbook _color = escape _xml ( collection . color + ( collection . color ? "ff" : "" ) ) ;
addressbook _description = escape _xml ( collection . description ) ;
resourcetype = '<CR:addressbook />' ;
2024-03-05 23:57:58 +11:00
} else if ( collection . type === CollectionType . WEBCAL ) {
calendar _color = escape _xml ( collection . color + ( collection . color ? "ff" : "" ) ) ;
calendar _description = escape _xml ( collection . description ) ;
resourcetype = '<CS:subscribed />' ;
2024-10-08 08:06:06 +02:00
calendar _source = escape _xml ( collection . source ) ;
2017-05-31 13:18:42 +02:00
} else {
calendar _color = escape _xml ( collection . color + ( collection . color ? "ff" : "" ) ) ;
calendar _description = escape _xml ( collection . description ) ;
resourcetype = '<C:calendar />' ;
if ( CollectionType . is _subset ( CollectionType . CALENDAR , collection . type ) ) {
components += '<C:comp name="VEVENT" />' ;
}
if ( CollectionType . is _subset ( CollectionType . JOURNAL , collection . type ) ) {
components += '<C:comp name="VJOURNAL" />' ;
}
if ( CollectionType . is _subset ( CollectionType . TASKS , collection . type ) ) {
components += '<C:comp name="VTODO" />' ;
}
}
2020-01-16 03:58:29 +01:00
let xml _request = create ? "mkcol" : "propertyupdate" ;
2017-05-31 13:18:42 +02:00
request . send ( '<?xml version="1.0" encoding="UTF-8" ?>' +
2024-03-06 01:50:26 +11:00
'<' + 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/">' +
2017-05-31 13:18:42 +02:00
'<set>' +
'<prop>' +
( create ? '<resourcetype><collection />' + resourcetype + '</resourcetype>' : '' ) +
2017-07-22 21:25:38 +02:00
( components ? '<C:supported-calendar-component-set>' + components + '</C:supported-calendar-component-set>' : '' ) +
( displayname ? '<displayname>' + displayname + '</displayname>' : '' ) +
( calendar _color ? '<I:calendar-color>' + calendar _color + '</I:calendar-color>' : '' ) +
( 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>' : '' ) +
2024-03-05 23:57:58 +11:00
( calendar _source ? '<CS:source>' + calendar _source + '</CS:source>' : '' ) +
2017-05-31 13:18:42 +02:00
'</prop>' +
'</set>' +
2017-07-22 21:25:38 +02:00
( ! create ? ( '<remove>' +
'<prop>' +
( ! components ? '<C:supported-calendar-component-set />' : '' ) +
( ! displayname ? '<displayname />' : '' ) +
( ! calendar _color ? '<I:calendar-color />' : '' ) +
( ! addressbook _color ? '<INF:addressbook-color />' : '' ) +
( ! addressbook _description ? '<CR:addressbook-description />' : '' ) +
( ! calendar _description ? '<C:calendar-description />' : '' ) +
'</prop>' +
'</remove>' ) : '' ) +
2017-05-31 13:18:42 +02:00
'</' + xml _request + '>' ) ;
return request ;
}
/ * *
* @ param { string } user
* @ param { string } password
* @ param { Collection } collection
* @ param { function ( ? string ) } callback Returns error or null
* @ return { XMLHttpRequest }
* /
function create _collection ( user , password , collection , callback ) {
return create _edit _collection ( user , password , collection , true , callback ) ;
}
/ * *
* @ param { string } user
* @ param { string } password
* @ param { Collection } collection
* @ param { function ( ? string ) } callback Returns error or null
* @ return { XMLHttpRequest }
* /
function edit _collection ( user , password , collection , callback ) {
return create _edit _collection ( user , password , collection , false , callback ) ;
}
2018-08-18 12:56:42 +02:00
/ * *
* @ return { string }
* /
function random _uuid ( ) {
2020-05-15 23:34:31 +02:00
return random _hex ( 8 ) + "-" + random _hex ( 4 ) + "-" + random _hex ( 4 ) + "-" + random _hex ( 4 ) + "-" + random _hex ( 12 ) ;
2018-08-18 12:56:42 +02:00
}
2017-05-31 13:18:42 +02:00
/ * *
* @ interface
* /
function Scene ( ) { }
/ * *
* Scene is on top of stack and visible .
* /
Scene . prototype . show = function ( ) { } ;
/ * *
* Scene is no longer visible .
* /
Scene . prototype . hide = function ( ) { } ;
/ * *
* Scene is removed from scene stack .
* /
Scene . prototype . release = function ( ) { } ;
/ * *
* @ type { Array < Scene > }
* /
2020-01-16 03:58:29 +01:00
let scene _stack = [ ] ;
2017-05-31 13:18:42 +02:00
/ * *
* Push scene onto stack .
* @ param { Scene } scene
* @ param { boolean } replace Replace the scene on top of the stack .
* /
function push _scene ( scene , replace ) {
if ( scene _stack . length >= 1 ) {
scene _stack [ scene _stack . length - 1 ] . hide ( ) ;
if ( replace ) {
scene _stack . pop ( ) . release ( ) ;
}
}
scene _stack . push ( scene ) ;
scene . show ( ) ;
}
/ * *
* Remove scenes from stack .
* @ param { number } index New top of stack
* /
function pop _scene ( index ) {
if ( scene _stack . length - 1 <= index ) {
return ;
}
scene _stack [ scene _stack . length - 1 ] . hide ( ) ;
while ( scene _stack . length - 1 > index ) {
2020-01-16 03:58:29 +01:00
let old _length = scene _stack . length ;
2017-05-31 13:18:42 +02:00
scene _stack . pop ( ) . release ( ) ;
if ( old _length - 1 === index + 1 ) {
break ;
}
}
if ( scene _stack . length >= 1 ) {
2020-01-16 03:58:29 +01:00
let scene = scene _stack [ scene _stack . length - 1 ] ;
2017-05-31 13:18:42 +02:00
scene . show ( ) ;
} else {
throw "Scene stack is empty" ;
}
}
/ * *
* @ constructor
* @ implements { Scene }
* /
function LoginScene ( ) {
2020-01-16 03:58:29 +01:00
let html _scene = document . getElementById ( "loginscene" ) ;
2020-01-16 03:58:31 +01:00
let form = html _scene . querySelector ( "[data-name=form]" ) ;
let user _form = html _scene . querySelector ( "[data-name=user]" ) ;
let password _form = html _scene . querySelector ( "[data-name=password]" ) ;
let error _form = html _scene . querySelector ( "[data-name=error]" ) ;
2020-01-16 03:58:29 +01:00
let logout _view = document . getElementById ( "logoutview" ) ;
2020-01-16 03:58:31 +01:00
let logout _user _form = logout _view . querySelector ( "[data-name=user]" ) ;
2024-03-17 12:30:15 +11:00
let logout _btn = logout _view . querySelector ( "[data-name=logout]" ) ;
let refresh _btn = logout _view . querySelector ( "[data-name=refresh]" ) ;
2017-05-31 13:18:42 +02:00
2020-01-16 03:58:29 +01:00
/** @type {?number} */ let scene _index = null ;
let user = "" ;
let error = "" ;
/** @type {?XMLHttpRequest} */ let principal _req = null ;
2017-05-31 13:18:42 +02:00
function read _form ( ) {
user = user _form . value ;
}
function fill _form ( ) {
user _form . value = user ;
password _form . value = "" ;
2023-08-31 20:42:41 +10:00
if ( error ) {
error _form . textContent = "Error: " + error ;
error _form . classList . remove ( "hidden" ) ;
} else {
error _form . classList . add ( "hidden" ) ;
}
2017-05-31 13:18:42 +02:00
}
function onlogin ( ) {
try {
read _form ( ) ;
2020-01-16 03:58:29 +01:00
let password = password _form . value ;
2017-05-31 13:18:42 +02:00
if ( user ) {
error = "" ;
// setup logout
2020-01-16 03:58:30 +01:00
logout _view . classList . remove ( "hidden" ) ;
2017-05-31 13:18:42 +02:00
logout _btn . onclick = onlogout ;
2024-03-17 12:30:15 +11:00
refresh _btn . onclick = refresh ;
2023-08-31 20:42:41 +10:00
logout _user _form . textContent = user + "'s Collections" ;
2017-05-31 13:18:42 +02:00
// Fetch principal
2020-01-16 03:58:29 +01:00
let loading _scene = new LoadingScene ( ) ;
2017-05-31 13:18:42 +02:00
push _scene ( loading _scene , false ) ;
principal _req = get _principal ( user , password , function ( collection , error1 ) {
if ( scene _index === null ) {
return ;
}
principal _req = null ;
if ( error1 ) {
error = error1 ;
pop _scene ( scene _index ) ;
} else {
// show collections
2020-01-16 03:58:29 +01:00
let saved _user = user ;
2017-05-31 13:18:42 +02:00
user = "" ;
2020-01-16 03:58:29 +01:00
let collections _scene = new CollectionsScene (
2017-05-31 13:18:42 +02:00
saved _user , password , collection , function ( error1 ) {
error = error1 ;
user = saved _user ;
} ) ;
push _scene ( collections _scene , true ) ;
}
} ) ;
} else {
error = "Username is empty" ;
fill _form ( ) ;
}
} catch ( err ) {
console . error ( err ) ;
}
return false ;
}
function onlogout ( ) {
try {
if ( scene _index === null ) {
return false ;
}
user = "" ;
pop _scene ( scene _index ) ;
} catch ( err ) {
console . error ( err ) ;
}
return false ;
}
2018-08-21 18:43:42 +02:00
function remove _logout ( ) {
2020-01-16 03:58:30 +01:00
logout _view . classList . add ( "hidden" ) ;
2018-08-21 18:43:42 +02:00
logout _btn . onclick = null ;
2024-03-17 12:30:15 +11:00
refresh _btn . onclick = null ;
2018-08-21 18:43:42 +02:00
logout _user _form . textContent = "" ;
}
2024-03-17 12:30:15 +11:00
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 ) ;
}
2017-05-31 13:18:42 +02:00
this . show = function ( ) {
2018-08-21 18:43:42 +02:00
remove _logout ( ) ;
2017-05-31 13:18:42 +02:00
fill _form ( ) ;
form . onsubmit = onlogin ;
2020-01-16 03:58:30 +01:00
html _scene . classList . remove ( "hidden" ) ;
2018-08-21 18:43:42 +02:00
scene _index = scene _stack . length - 1 ;
2020-05-03 21:00:48 +02:00
user _form . focus ( ) ;
2017-05-31 13:18:42 +02:00
} ;
this . hide = function ( ) {
read _form ( ) ;
2020-01-16 03:58:30 +01:00
html _scene . classList . add ( "hidden" ) ;
2017-05-31 13:18:42 +02:00
form . onsubmit = null ;
} ;
this . release = function ( ) {
scene _index = null ;
// cancel pending requests
if ( principal _req !== null ) {
principal _req . abort ( ) ;
principal _req = null ;
}
2018-08-21 18:43:42 +02:00
remove _logout ( ) ;
2017-05-31 13:18:42 +02:00
} ;
}
/ * *
* @ constructor
* @ implements { Scene }
* /
function LoadingScene ( ) {
2020-01-16 03:58:29 +01:00
let html _scene = document . getElementById ( "loadingscene" ) ;
2017-05-31 13:18:42 +02:00
this . show = function ( ) {
2020-01-16 03:58:30 +01:00
html _scene . classList . remove ( "hidden" ) ;
2017-05-31 13:18:42 +02:00
} ;
this . hide = function ( ) {
2020-01-16 03:58:30 +01:00
html _scene . classList . add ( "hidden" ) ;
2017-05-31 13:18:42 +02:00
} ;
this . release = function ( ) { } ;
}
/ * *
* @ constructor
* @ implements { Scene }
* @ param { string } user
* @ param { string } password
* @ param { Collection } collection The principal collection .
* @ param { function ( string ) } onerror Called when an error occurs , before the
* scene is popped .
* /
function CollectionsScene ( user , password , collection , onerror ) {
2020-01-16 03:58:29 +01:00
let html _scene = document . getElementById ( "collectionsscene" ) ;
2020-01-16 03:58:31 +01:00
let template = html _scene . querySelector ( "[data-name=collectiontemplate]" ) ;
let new _btn = html _scene . querySelector ( "[data-name=new]" ) ;
let upload _btn = html _scene . querySelector ( "[data-name=upload]" ) ;
2017-05-31 13:18:42 +02:00
2020-01-16 03:58:29 +01:00
/** @type {?number} */ let scene _index = null ;
/** @type {?XMLHttpRequest} */ let collections _req = null ;
/** @type {?Array<Collection>} */ let collections = null ;
/** @type {Array<Node>} */ let nodes = [ ] ;
2017-05-31 13:18:42 +02:00
function onnew ( ) {
try {
2020-01-16 03:58:29 +01:00
let create _collection _scene = new CreateEditCollectionScene ( user , password , collection ) ;
2017-05-31 13:18:42 +02:00
push _scene ( create _collection _scene , false ) ;
} catch ( err ) {
console . error ( err ) ;
}
return false ;
}
2018-08-18 12:56:42 +02:00
function onupload ( ) {
try {
2024-03-17 12:30:15 +11:00
let upload _scene = new UploadCollectionScene ( user , password , collection ) ;
push _scene ( upload _scene ) ;
2018-08-18 12:56:42 +02:00
} catch ( err ) {
console . error ( err ) ;
}
return false ;
}
2017-05-31 13:18:42 +02:00
function onedit ( collection ) {
try {
2020-01-16 03:58:29 +01:00
let edit _collection _scene = new CreateEditCollectionScene ( user , password , collection ) ;
2017-05-31 13:18:42 +02:00
push _scene ( edit _collection _scene , false ) ;
} catch ( err ) {
console . error ( err ) ;
}
return false ;
}
function ondelete ( collection ) {
try {
2020-01-16 03:58:29 +01:00
let delete _collection _scene = new DeleteCollectionScene ( user , password , collection ) ;
2017-05-31 13:18:42 +02:00
push _scene ( delete _collection _scene , false ) ;
} catch ( err ) {
console . error ( err ) ;
}
return false ;
}
function show _collections ( collections ) {
2024-03-17 12:30:15 +11:00
let heightOfNavBar = document . querySelector ( "#logoutview" ) . offsetHeight + "px" ;
html _scene . style . marginTop = heightOfNavBar ;
html _scene . style . height = "calc(100vh - " + heightOfNavBar + ")" ;
2017-05-31 13:18:42 +02:00
collections . forEach ( function ( collection ) {
2020-01-16 03:58:29 +01:00
let node = template . cloneNode ( true ) ;
2020-01-16 03:58:30 +01:00
node . classList . remove ( "hidden" ) ;
2020-01-16 03:58:31 +01:00
let title _form = node . querySelector ( "[data-name=title]" ) ;
let description _form = node . querySelector ( "[data-name=description]" ) ;
2024-03-13 07:42:47 +11:00
let contentcount _form = node . querySelector ( "[data-name=contentcount]" ) ;
2020-01-16 03:58:31 +01:00
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]" ) ;
2023-08-31 20:42:41 +10:00
let download _btn = node . querySelector ( "[data-name=download]" ) ;
2017-05-31 13:18:42 +02:00
if ( collection . color ) {
2023-08-31 20:42:41 +10:00
color _form . style . background = collection . color ;
2017-05-31 13:18:42 +02:00
}
2024-03-05 23:57:58 +11:00
let possible _types = [ CollectionType . ADDRESSBOOK , CollectionType . WEBCAL ] ;
2017-05-31 13:18:42 +02:00
[ CollectionType . CALENDAR , "" ] . forEach ( function ( e ) {
[ CollectionType . union ( e , CollectionType . JOURNAL ) , e ] . forEach ( function ( e ) {
[ CollectionType . union ( e , CollectionType . TASKS ) , e ] . forEach ( function ( e ) {
if ( e ) {
possible _types . push ( e ) ;
}
} ) ;
} ) ;
} ) ;
possible _types . forEach ( function ( e ) {
if ( e !== collection . type ) {
2020-01-16 03:58:31 +01:00
node . querySelector ( "[data-name=" + e + "]" ) . classList . add ( "hidden" ) ;
2017-05-31 13:18:42 +02:00
}
} ) ;
title _form . textContent = collection . displayname || collection . href ;
2023-08-31 20:42:41 +10:00
if ( title _form . textContent . length > 30 ) {
title _form . classList . add ( "smalltext" ) ;
}
2017-05-31 13:18:42 +02:00
description _form . textContent = collection . description ;
2023-08-31 20:42:41 +10:00
if ( description _form . textContent . length > 150 ) {
description _form . classList . add ( "smalltext" ) ;
}
2024-03-13 07:42:47 +11:00
if ( collection . type != CollectionType . WEBCAL ) {
2024-03-13 05:07:22 +11:00
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 ;
2024-03-13 07:42:47 +11:00
}
2020-01-16 03:58:29 +01:00
let href = SERVER + collection . href ;
2023-08-31 20:42:41 +10:00
url _form . value = href ;
download _btn . href = href ;
2024-03-11 17:13:37 +11:00
if ( collection . type == CollectionType . WEBCAL ) {
download _btn . parentElement . classList . add ( "hidden" ) ;
}
2020-04-22 19:20:30 +02:00
delete _btn . onclick = function ( ) { return ondelete ( collection ) ; } ;
edit _btn . onclick = function ( ) { return onedit ( collection ) ; } ;
2020-01-16 03:58:30 +01:00
node . classList . remove ( "hidden" ) ;
2017-05-31 13:18:42 +02:00
nodes . push ( node ) ;
template . parentNode . insertBefore ( node , template ) ;
} ) ;
}
function update ( ) {
2020-01-16 03:58:29 +01:00
let loading _scene = new LoadingScene ( ) ;
2018-08-21 18:43:42 +02:00
push _scene ( loading _scene , false ) ;
2017-05-31 13:18:42 +02:00
collections _req = get _collections ( user , password , collection , function ( collections1 , error ) {
if ( scene _index === null ) {
return ;
}
collections _req = null ;
if ( error ) {
onerror ( error ) ;
pop _scene ( scene _index - 1 ) ;
} else {
collections = collections1 ;
2018-08-21 18:43:42 +02:00
pop _scene ( scene _index ) ;
2017-05-31 13:18:42 +02:00
}
} ) ;
}
this . show = function ( ) {
2020-01-16 03:58:30 +01:00
html _scene . classList . remove ( "hidden" ) ;
2017-05-31 13:18:42 +02:00
new _btn . onclick = onnew ;
2018-08-18 12:56:42 +02:00
upload _btn . onclick = onupload ;
2018-08-21 18:43:42 +02:00
if ( collections === null ) {
2017-05-31 13:18:42 +02:00
update ( ) ;
} else {
2018-08-21 18:43:42 +02:00
// from update loading scene
show _collections ( collections ) ;
2017-05-31 13:18:42 +02:00
}
} ;
this . hide = function ( ) {
2020-01-16 03:58:30 +01:00
html _scene . classList . add ( "hidden" ) ;
2018-08-21 18:43:42 +02:00
scene _index = scene _stack . length - 1 ;
2017-05-31 13:18:42 +02:00
new _btn . onclick = null ;
2018-08-18 12:56:42 +02:00
upload _btn . onclick = null ;
2018-08-21 18:43:42 +02:00
collections = null ;
// remove collection
nodes . forEach ( function ( node ) {
2020-01-16 03:58:30 +01:00
node . parentNode . removeChild ( node ) ;
2018-08-21 18:43:42 +02:00
} ) ;
nodes = [ ] ;
2017-05-31 13:18:42 +02:00
} ;
this . release = function ( ) {
scene _index = null ;
if ( collections _req !== null ) {
collections _req . abort ( ) ;
collections _req = null ;
}
2018-08-21 18:43:42 +02:00
collections = null ;
2018-08-18 12:56:42 +02:00
} ;
}
/ * *
* @ constructor
* @ implements { Scene }
* @ param { string } user
* @ param { string } password
* @ param { Collection } collection parent collection
* @ param { Array < File > } files
* /
2024-03-17 12:30:15 +11:00
function UploadCollectionScene ( user , password , collection ) {
2020-01-16 03:58:29 +01:00
let html _scene = document . getElementById ( "uploadcollectionscene" ) ;
2020-01-16 03:58:31 +01:00
let template = html _scene . querySelector ( "[data-name=filetemplate]" ) ;
2024-03-17 12:30:15 +11:00
let upload _btn = html _scene . querySelector ( "[data-name=submit]" ) ;
2020-01-16 03:58:31 +01:00
let close _btn = html _scene . querySelector ( "[data-name=close]" ) ;
2024-03-17 12:30:15 +11:00
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 ;
2025-02-27 08:32:26 +01:00
href _form . value = "" ;
2018-08-18 12:56:42 +02:00
2020-01-16 03:58:29 +01:00
/** @type {?number} */ let scene _index = null ;
/** @type {?XMLHttpRequest} */ let upload _req = null ;
2024-03-17 12:30:15 +11:00
/** @type {Array<string>} */ let results = [ ] ;
2020-01-16 03:58:29 +01:00
/** @type {?Array<Node>} */ let nodes = null ;
2018-08-18 12:56:42 +02:00
2024-03-17 12:30:15 +11:00
function upload _start ( ) {
2018-08-18 12:56:42 +02:00
try {
2024-03-17 12:30:15 +11:00
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 ;
2018-08-18 12:56:42 +02:00
} else {
2024-03-17 12:30:15 +11:00
let file = files [ results . length ] ;
if ( files . length > 1 || href . length == 0 ) {
href = random _uuid ( ) ;
}
2025-02-27 08:26:19 +01:00
let upload _href = collection . href + href + "/" ;
2024-03-17 12:30:15 +11:00
upload _req = upload _collection ( user , password , upload _href , file , function ( result ) {
2018-08-18 12:56:42 +02:00
upload _req = null ;
2024-03-17 12:30:15 +11:00
results . push ( result ) ;
updateFileStatus ( results . length - 1 ) ;
2018-08-18 12:56:42 +02:00
upload _next ( ) ;
} ) ;
}
2024-03-17 12:30:15 +11:00
} catch ( err ) {
2018-08-18 12:56:42 +02:00
console . error ( err ) ;
}
}
function onclose ( ) {
try {
pop _scene ( scene _index - 1 ) ;
} catch ( err ) {
console . error ( err ) ;
}
return false ;
}
function updateFileStatus ( i ) {
if ( nodes === null ) {
return ;
}
2020-01-16 03:58:31 +01:00
let success _form = nodes [ i ] . querySelector ( "[data-name=success]" ) ;
let error _form = nodes [ i ] . querySelector ( "[data-name=error]" ) ;
2024-03-17 12:30:15 +11:00
if ( results . length > i ) {
if ( results [ i ] ) {
2020-01-16 03:58:30 +01:00
success _form . classList . add ( "hidden" ) ;
2024-03-17 12:30:15 +11:00
error _form . textContent = "Error: " + results [ i ] ;
2020-01-16 03:58:30 +01:00
error _form . classList . remove ( "hidden" ) ;
2018-08-18 12:56:42 +02:00
} else {
2020-01-16 03:58:30 +01:00
success _form . classList . remove ( "hidden" ) ;
error _form . classList . add ( "hidden" ) ;
2018-08-18 12:56:42 +02:00
}
} else {
2020-01-16 03:58:30 +01:00
success _form . classList . add ( "hidden" ) ;
error _form . classList . add ( "hidden" ) ;
2018-08-18 12:56:42 +02:00
}
}
2024-03-17 12:30:15 +11:00
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 ;
2018-08-18 12:56:42 +02:00
}
2024-03-17 12:30:15 +11:00
href = newhreftxtvalue ;
if ( uploadfile _form . files . length == 0 ) {
alert ( "You must select at least one file to upload" ) ;
return false ;
2018-08-18 12:56:42 +02:00
}
2024-03-17 12:30:15 +11:00
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" ) ;
2024-03-18 21:01:21 +01:00
href _label . classList . add ( "hidden" ) ;
2025-02-27 08:32:26 +01:00
href _form . value = random _uuid ( ) ; // dummy, will be replaced on upload
2024-03-17 12:30:15 +11:00
} else {
hreflimitmsg _html . classList . add ( "hidden" ) ;
href _form . classList . remove ( "hidden" ) ;
2024-03-18 21:01:21 +01:00
href _label . classList . remove ( "hidden" ) ;
2025-02-27 08:32:26 +01:00
href _form . value = files [ 0 ] . name . replace ( /\.(ics|vcf)$/ , '' ) ;
2018-08-18 12:56:42 +02:00
}
2024-03-17 12:30:15 +11:00
return false ;
}
this . show = function ( ) {
scene _index = scene _stack . length - 1 ;
html _scene . classList . remove ( "hidden" ) ;
close _btn . onclick = onclose ;
2025-02-27 19:40:24 +01:00
if ( error ) {
error _form . textContent = "Error: " + error ;
error _form . classList . remove ( "hidden" ) ;
} else {
error _form . classList . add ( "hidden" ) ;
}
2018-08-18 12:56:42 +02:00
} ;
this . hide = function ( ) {
2020-01-16 03:58:30 +01:00
html _scene . classList . add ( "hidden" ) ;
close _btn . classList . remove ( "hidden" ) ;
2024-03-17 12:30:15 +11:00
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" ) ;
2018-08-18 12:56:42 +02:00
close _btn . onclick = null ;
2024-03-17 12:30:15 +11:00
upload _btn . onclick = null ;
href _form . value = "" ;
uploadfile _form . value = "" ;
if ( nodes == null ) {
return ;
}
2018-08-18 12:56:42 +02:00
nodes . forEach ( function ( node ) {
2020-01-16 03:58:30 +01:00
node . parentNode . removeChild ( node ) ;
2018-08-18 12:56:42 +02:00
} ) ;
nodes = null ;
} ;
this . release = function ( ) {
scene _index = null ;
if ( upload _req !== null ) {
upload _req . abort ( ) ;
upload _req = null ;
}
2017-05-31 13:18:42 +02:00
} ;
}
/ * *
* @ constructor
* @ implements { Scene }
* @ param { string } user
* @ param { string } password
* @ param { Collection } collection
* /
function DeleteCollectionScene ( user , password , collection ) {
2020-01-16 03:58:29 +01:00
let html _scene = document . getElementById ( "deletecollectionscene" ) ;
2020-01-16 03:58:31 +01:00
let title _form = html _scene . querySelector ( "[data-name=title]" ) ;
let error _form = html _scene . querySelector ( "[data-name=error]" ) ;
2024-03-13 07:42:47 +11:00
let confirmation _txt = html _scene . querySelector ( "[data-name=confirmationtxt]" ) ;
let delete _confirmation _lbl = html _scene . querySelector ( "[data-name=deleteconfirmationtext]" ) ;
2020-01-16 03:58:31 +01:00
let delete _btn = html _scene . querySelector ( "[data-name=delete]" ) ;
let cancel _btn = html _scene . querySelector ( "[data-name=cancel]" ) ;
2017-05-31 13:18:42 +02:00
2024-03-13 07:42:47 +11:00
delete _confirmation _lbl . innerHTML = DELETE _CONFIRMATION _TEXT ;
confirmation _txt . value = "" ;
confirmation _txt . addEventListener ( "keydown" , onkeydown ) ;
2020-01-16 03:58:29 +01:00
/** @type {?number} */ let scene _index = null ;
/** @type {?XMLHttpRequest} */ let delete _req = null ;
let error = "" ;
2017-05-31 13:18:42 +02:00
function ondelete ( ) {
2024-03-13 07:42:47 +11:00
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 ;
}
2017-05-31 13:18:42 +02:00
try {
2020-01-16 03:58:29 +01:00
let loading _scene = new LoadingScene ( ) ;
2017-05-31 13:18:42 +02:00
push _scene ( loading _scene ) ;
delete _req = delete _collection ( user , password , collection , function ( error1 ) {
if ( scene _index === null ) {
return ;
}
delete _req = null ;
if ( error1 ) {
error = error1 ;
pop _scene ( scene _index ) ;
} else {
pop _scene ( scene _index - 1 ) ;
}
} ) ;
} catch ( err ) {
console . error ( err ) ;
}
return false ;
}
function oncancel ( ) {
try {
pop _scene ( scene _index - 1 ) ;
} catch ( err ) {
console . error ( err ) ;
}
return false ;
}
2024-03-13 07:42:47 +11:00
function onkeydown ( event ) {
if ( event . keyCode !== 13 ) {
return ;
}
ondelete ( ) ;
}
2017-05-31 13:18:42 +02:00
this . show = function ( ) {
this . release ( ) ;
scene _index = scene _stack . length - 1 ;
2020-01-16 03:58:30 +01:00
html _scene . classList . remove ( "hidden" ) ;
2017-05-31 13:18:42 +02:00
title _form . textContent = collection . displayname || collection . href ;
delete _btn . onclick = ondelete ;
cancel _btn . onclick = oncancel ;
2023-08-31 20:42:41 +10:00
if ( error ) {
error _form . textContent = "Error: " + error ;
error _form . classList . remove ( "hidden" ) ;
} else {
error _form . classList . add ( "hidden" ) ;
}
2017-05-31 13:18:42 +02:00
} ;
this . hide = function ( ) {
2020-01-16 03:58:30 +01:00
html _scene . classList . add ( "hidden" ) ;
2017-05-31 13:18:42 +02:00
cancel _btn . onclick = null ;
delete _btn . onclick = null ;
} ;
this . release = function ( ) {
scene _index = null ;
if ( delete _req !== null ) {
delete _req . abort ( ) ;
delete _req = null ;
}
} ;
}
/ * *
* Generate random hex number .
* @ param { number } length
* @ return { string }
* /
2020-05-15 23:34:31 +02:00
function random _hex ( length ) {
let bytes = new Uint8Array ( Math . ceil ( length / 2 ) ) ;
window . crypto . getRandomValues ( bytes ) ;
return bytes . reduce ( ( s , b ) => s + b . toString ( 16 ) . padStart ( 2 , "0" ) , "" ) . substring ( 0 , length ) ;
2017-05-31 13:18:42 +02:00
}
/ * *
* @ constructor
* @ implements { Scene }
* @ param { string } user
* @ param { string } password
* @ param { Collection } collection if it ' s a principal collection , a new
* collection will be created inside of it .
* Otherwise the collection will be edited .
* /
function CreateEditCollectionScene ( user , password , collection ) {
2020-01-16 03:58:29 +01:00
let edit = collection . type !== CollectionType . PRINCIPAL ;
let html _scene = document . getElementById ( edit ? "editcollectionscene" : "createcollectionscene" ) ;
2020-01-16 03:58:31 +01:00
let title _form = edit ? html _scene . querySelector ( "[data-name=title]" ) : null ;
let error _form = html _scene . querySelector ( "[data-name=error]" ) ;
2024-03-13 07:42:47 +11:00
let href _form = html _scene . querySelector ( "[data-name=href]" ) ;
let href _label = html _scene . querySelector ( "label[for=href]" ) ;
2020-01-16 03:58:31 +01:00
let displayname _form = html _scene . querySelector ( "[data-name=displayname]" ) ;
2024-03-05 23:57:58 +11:00
let displayname _label = html _scene . querySelector ( "label[for=displayname]" ) ;
2020-01-16 03:58:31 +01:00
let description _form = html _scene . querySelector ( "[data-name=description]" ) ;
2024-03-05 23:57:58 +11:00
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]" ) ;
2020-01-16 03:58:31 +01:00
let type _form = html _scene . querySelector ( "[data-name=type]" ) ;
2024-03-05 23:57:58 +11:00
let type _label = html _scene . querySelector ( "label[for=type]" ) ;
2020-01-16 03:58:31 +01:00
let color _form = html _scene . querySelector ( "[data-name=color]" ) ;
2024-03-05 23:57:58 +11:00
let color _label = html _scene . querySelector ( "label[for=color]" ) ;
2020-01-16 03:58:31 +01:00
let submit _btn = html _scene . querySelector ( "[data-name=submit]" ) ;
let cancel _btn = html _scene . querySelector ( "[data-name=cancel]" ) ;
2017-05-31 13:18:42 +02:00
2024-03-05 23:57:58 +11:00
2020-01-16 03:58:29 +01:00
/** @type {?number} */ let scene _index = null ;
/** @type {?XMLHttpRequest} */ let create _edit _req = null ;
let error = "" ;
/** @type {?Element} */ let saved _type _form = null ;
2017-05-31 13:18:42 +02:00
2020-01-16 03:58:29 +01:00
let href = edit ? collection . href : collection . href + random _uuid ( ) + "/" ;
let displayname = edit ? collection . displayname : "" ;
let description = edit ? collection . description : "" ;
2024-03-05 23:57:58 +11:00
let source = edit ? collection . source : "" ;
2020-01-16 03:58:29 +01:00
let type = edit ? collection . type : CollectionType . CALENDAR _JOURNAL _TASKS ;
2020-05-15 23:34:31 +02:00
let color = edit && collection . color ? collection . color : "#" + random _hex ( 6 ) ;
2017-05-31 13:18:42 +02:00
2024-03-13 07:42:47 +11:00
if ( ! edit ) {
href _form . addEventListener ( "keydown" , cleanHREFinput ) ;
}
2017-05-31 13:18:42 +02:00
function remove _invalid _types ( ) {
if ( ! edit ) {
return ;
}
2020-01-16 03:58:29 +01:00
/** @type {HTMLOptionsCollection} */ let options = type _form . options ;
2017-05-31 13:18:42 +02:00
// remove all options that are not supersets
2024-03-13 07:42:47 +11:00
let valid _type _options = CollectionType . valid _options _for _type ( type ) ;
2020-01-16 03:58:29 +01:00
for ( let i = options . length - 1 ; i >= 0 ; i -- ) {
2024-03-13 07:42:47 +11:00
if ( valid _type _options . indexOf ( options [ i ] . value ) < 0 ) {
2017-05-31 13:18:42 +02:00
options . remove ( i ) ;
}
}
}
function read _form ( ) {
2024-03-13 07:42:47 +11:00
if ( ! edit ) {
2024-03-17 12:30:15 +11:00
cleanHREFinput ( href _form ) ;
2024-03-13 07:42:47 +11:00
let newhreftxtvalue = href _form . value . trim ( ) . toLowerCase ( ) ;
if ( ! isValidHREF ( newhreftxtvalue ) ) {
alert ( "You must enter a valid HREF" ) ;
return false ;
}
2025-02-27 08:09:05 +01:00
href = collection . href + newhreftxtvalue + "/" ;
2024-03-13 07:42:47 +11:00
}
2017-05-31 13:18:42 +02:00
displayname = displayname _form . value ;
description = description _form . value ;
2024-03-05 23:57:58 +11:00
source = source _form . value ;
2017-05-31 13:18:42 +02:00
type = type _form . value ;
color = color _form . value ;
2024-03-13 07:42:47 +11:00
return true ;
2017-05-31 13:18:42 +02:00
}
function fill _form ( ) {
2024-03-13 07:42:47 +11:00
if ( ! edit ) {
href _form . value = random _uuid ( ) ;
}
2017-05-31 13:18:42 +02:00
displayname _form . value = displayname ;
description _form . value = description ;
2024-03-05 23:57:58 +11:00
source _form . value = source ;
2017-05-31 13:18:42 +02:00
type _form . value = type ;
color _form . value = color ;
2023-08-31 20:42:41 +10:00
if ( error ) {
error _form . textContent = "Error: " + error ;
error _form . classList . remove ( "hidden" ) ;
}
error _form . classList . add ( "hidden" ) ;
2024-03-05 23:57:58 +11:00
onTypeChange ( ) ;
type _form . addEventListener ( "change" , onTypeChange ) ;
2017-05-31 13:18:42 +02:00
}
function onsubmit ( ) {
try {
2024-03-13 07:42:47 +11:00
if ( ! read _form ( ) ) {
return false ;
}
2020-01-16 03:58:29 +01:00
let sane _color = color . trim ( ) ;
2017-05-31 13:18:42 +02:00
if ( sane _color ) {
2020-01-16 03:58:29 +01:00
let color _match = COLOR _RE . exec ( sane _color ) ;
2017-05-31 13:18:42 +02:00
if ( ! color _match ) {
error = "Invalid color" ;
fill _form ( ) ;
return false ;
}
sane _color = color _match [ 1 ] ;
}
2020-01-16 03:58:29 +01:00
let loading _scene = new LoadingScene ( ) ;
2017-05-31 13:18:42 +02:00
push _scene ( loading _scene ) ;
2024-03-13 05:07:22 +11:00
let collection = new Collection ( href , type , displayname , description , sane _color , 0 , 0 , source ) ;
2020-01-16 03:58:29 +01:00
let callback = function ( error1 ) {
2017-05-31 13:18:42 +02:00
if ( scene _index === null ) {
return ;
}
create _edit _req = null ;
if ( error1 ) {
error = error1 ;
pop _scene ( scene _index ) ;
} else {
pop _scene ( scene _index - 1 ) ;
}
} ;
if ( edit ) {
create _edit _req = edit _collection ( user , password , collection , callback ) ;
} else {
create _edit _req = create _collection ( user , password , collection , callback ) ;
}
} catch ( err ) {
console . error ( err ) ;
}
return false ;
}
function oncancel ( ) {
try {
pop _scene ( scene _index - 1 ) ;
} catch ( err ) {
console . error ( err ) ;
}
return false ;
}
2024-03-13 07:42:47 +11:00
2024-03-05 23:57:58 +11:00
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" ) ;
}
}
2017-05-31 13:18:42 +02:00
this . show = function ( ) {
this . release ( ) ;
scene _index = scene _stack . length - 1 ;
// Clone type_form because it's impossible to hide options without removing them
saved _type _form = type _form ;
type _form = type _form . cloneNode ( true ) ;
saved _type _form . parentNode . replaceChild ( type _form , saved _type _form ) ;
remove _invalid _types ( ) ;
2020-01-16 03:58:30 +01:00
html _scene . classList . remove ( "hidden" ) ;
2017-05-31 13:18:42 +02:00
if ( edit ) {
title _form . textContent = collection . displayname || collection . href ;
}
fill _form ( ) ;
submit _btn . onclick = onsubmit ;
cancel _btn . onclick = oncancel ;
2025-02-27 19:40:24 +01:00
if ( error ) {
error _form . textContent = "Error: " + error ;
error _form . classList . remove ( "hidden" ) ;
} else {
error _form . classList . add ( "hidden" ) ;
}
2017-05-31 13:18:42 +02:00
} ;
this . hide = function ( ) {
read _form ( ) ;
2020-01-16 03:58:30 +01:00
html _scene . classList . add ( "hidden" ) ;
2017-05-31 13:18:42 +02:00
// restore type_form
type _form . parentNode . replaceChild ( saved _type _form , type _form ) ;
type _form = saved _type _form ;
saved _type _form = null ;
submit _btn . onclick = null ;
cancel _btn . onclick = null ;
} ;
this . release = function ( ) {
scene _index = null ;
if ( create _edit _req !== null ) {
create _edit _req . abort ( ) ;
create _edit _req = null ;
}
} ;
}
2024-03-18 06:40:14 +11:00
/ * *
* Removed invalid HREF characters for a collection HREF .
2024-03-18 21:01:21 +01:00
*
2024-03-18 06:40:14 +11:00
* @ 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 ( ) ;
2025-02-27 07:50:41 +01:00
//Clean the HREF to remove not permitted chars
currentTxtVal = currentTxtVal . replace ( /(?![0-9a-z\-\_\.])./g , '' ) ;
//Clean the HREF to remove leading . (would result in hidden directory)
currentTxtVal = currentTxtVal . replace ( /^\./ , '' ) ;
2024-03-18 06:40:14 +11:00
href _form . value = currentTxtVal ;
}
/ * *
* Checks if a proposed HREF for a collection has a valid format and syntax .
2024-03-18 21:01:21 +01:00
*
2024-03-18 06:40:14 +11:00
* @ param href String of the porposed HREF .
2024-03-18 21:01:21 +01:00
*
2024-03-18 06:40:14 +11:00
* @ 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 ;
}
2024-03-13 05:07:22 +11:00
/ * *
* Format bytes to human - readable text .
2024-03-14 06:08:47 +01:00
*
2024-03-13 05:07:22 +11:00
* @ param bytes Number of bytes .
2024-03-14 06:08:47 +01:00
*
2024-03-13 05:07:22 +11:00
* @ 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 ] ;
}
2024-03-14 06:08:47 +01:00
2024-03-13 05:07:22 +11:00
2017-05-31 13:18:42 +02:00
function main ( ) {
2020-01-16 04:39:22 +01:00
// Hide startup loading message
document . getElementById ( "loadingscene" ) . classList . add ( "hidden" ) ;
2017-05-31 13:18:42 +02:00
push _scene ( new LoginScene ( ) , false ) ;
}
window . addEventListener ( "load" , main ) ;