fn.js 51 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413
  1. /**
  2. * This file is part of Radicale Server - Calendar Server
  3. * Copyright © 2017-2024 Unrud <unrud@outlook.com>
  4. * Copyright © 2023-2024 Matthew Hana <matthew.hana@gmail.com>
  5. * Copyright © 2024-2025 Peter Bieringer <pb@bieringer.de>
  6. *
  7. * This program is free software: you can redistribute it and/or modify
  8. * it under the terms of the GNU General Public License as published by
  9. * the Free Software Foundation, either version 3 of the License, or
  10. * (at your option) any later version.
  11. *
  12. * This program is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU General Public License
  18. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. */
  20. /**
  21. * Server address
  22. * @const
  23. * @type {string}
  24. */
  25. const SERVER = location.origin;
  26. /**
  27. * Path of the root collection on the server (must end with /)
  28. * @const
  29. * @type {string}
  30. */
  31. const ROOT_PATH = location.pathname.replace(new RegExp("/+[^/]+/*(/index\\.html?)?$"), "") + '/';
  32. /**
  33. * Regex to match and normalize color
  34. * @const
  35. */
  36. const COLOR_RE = new RegExp("^(#[0-9A-Fa-f]{6})(?:[0-9A-Fa-f]{2})?$");
  37. /**
  38. * The text needed to confirm deleting a collection
  39. * @const
  40. */
  41. const DELETE_CONFIRMATION_TEXT = "DELETE";
  42. /**
  43. * Escape string for usage in XML
  44. * @param {string} s
  45. * @return {string}
  46. */
  47. function escape_xml(s) {
  48. return (s
  49. .replace(/&/g, "&amp;")
  50. .replace(/"/g, "&quot;")
  51. .replace(/'/g, "&apos;")
  52. .replace(/</g, "&lt;")
  53. .replace(/>/g, "&gt;"));
  54. }
  55. /**
  56. * @enum {string}
  57. */
  58. const CollectionType = {
  59. PRINCIPAL: "PRINCIPAL",
  60. ADDRESSBOOK: "ADDRESSBOOK",
  61. CALENDAR_JOURNAL_TASKS: "CALENDAR_JOURNAL_TASKS",
  62. CALENDAR_JOURNAL: "CALENDAR_JOURNAL",
  63. CALENDAR_TASKS: "CALENDAR_TASKS",
  64. JOURNAL_TASKS: "JOURNAL_TASKS",
  65. CALENDAR: "CALENDAR",
  66. JOURNAL: "JOURNAL",
  67. TASKS: "TASKS",
  68. WEBCAL: "WEBCAL",
  69. is_subset: function(a, b) {
  70. let components = a.split("_");
  71. for (let i = 0; i < components.length; i++) {
  72. if (b.search(components[i]) === -1) {
  73. return false;
  74. }
  75. }
  76. return true;
  77. },
  78. union: function(a, b) {
  79. if (a.search(this.ADDRESSBOOK) !== -1 || b.search(this.ADDRESSBOOK) !== -1) {
  80. if (a && a !== this.ADDRESSBOOK || b && b !== this.ADDRESSBOOK) {
  81. throw "Invalid union: " + a + " " + b;
  82. }
  83. return this.ADDRESSBOOK;
  84. }
  85. let union = [];
  86. if (a.search(this.CALENDAR) !== -1 || b.search(this.CALENDAR) !== -1) {
  87. union.push(this.CALENDAR);
  88. }
  89. if (a.search(this.JOURNAL) !== -1 || b.search(this.JOURNAL) !== -1) {
  90. union.push(this.JOURNAL);
  91. }
  92. if (a.search(this.TASKS) !== -1 || b.search(this.TASKS) !== -1) {
  93. union.push(this.TASKS);
  94. }
  95. if (a.search(this.WEBCAL) !== -1 || b.search(this.WEBCAL) !== -1) {
  96. union.push(this.WEBCAL);
  97. }
  98. return union.join("_");
  99. },
  100. valid_options_for_type: function(a){
  101. a = a.trim().toUpperCase();
  102. switch(a){
  103. case CollectionType.CALENDAR_JOURNAL_TASKS:
  104. case CollectionType.CALENDAR_JOURNAL:
  105. case CollectionType.CALENDAR_TASKS:
  106. case CollectionType.JOURNAL_TASKS:
  107. case CollectionType.CALENDAR:
  108. case CollectionType.JOURNAL:
  109. case CollectionType.TASKS:
  110. return [CollectionType.CALENDAR_JOURNAL_TASKS, CollectionType.CALENDAR_JOURNAL, CollectionType.CALENDAR_TASKS, CollectionType.JOURNAL_TASKS, CollectionType.CALENDAR, CollectionType.JOURNAL, CollectionType.TASKS];
  111. case CollectionType.ADDRESSBOOK:
  112. case CollectionType.WEBCAL:
  113. default:
  114. return [a];
  115. }
  116. }
  117. };
  118. /**
  119. * @constructor
  120. * @struct
  121. * @param {string} href Must always start and end with /.
  122. * @param {CollectionType} type
  123. * @param {string} displayname
  124. * @param {string} description
  125. * @param {string} color
  126. */
  127. function Collection(href, type, displayname, description, color, contentcount, size, source) {
  128. this.href = href;
  129. this.type = type;
  130. this.displayname = displayname;
  131. this.color = color;
  132. this.description = description;
  133. this.source = source;
  134. this.contentcount = contentcount;
  135. this.size = size;
  136. }
  137. /**
  138. * Find the principal collection.
  139. * @param {string} user
  140. * @param {string} password
  141. * @param {function(?Collection, ?string)} callback Returns result or error
  142. * @return {XMLHttpRequest}
  143. */
  144. function get_principal(user, password, callback) {
  145. let request = new XMLHttpRequest();
  146. request.open("PROPFIND", SERVER + ROOT_PATH, true, user, encodeURIComponent(password));
  147. request.onreadystatechange = function() {
  148. if (request.readyState !== 4) {
  149. return;
  150. }
  151. if (request.status === 207) {
  152. let xml = request.responseXML;
  153. let principal_element = xml.querySelector("*|multistatus:root > *|response:first-of-type > *|propstat > *|prop > *|current-user-principal > *|href");
  154. let displayname_element = xml.querySelector("*|multistatus:root > *|response:first-of-type > *|propstat > *|prop > *|displayname");
  155. if (principal_element) {
  156. callback(new Collection(
  157. principal_element.textContent,
  158. CollectionType.PRINCIPAL,
  159. displayname_element ? displayname_element.textContent : "",
  160. "",
  161. 0,
  162. ""), null);
  163. } else {
  164. callback(null, "Internal error");
  165. }
  166. } else {
  167. callback(null, request.status + " " + request.statusText);
  168. }
  169. };
  170. request.send('<?xml version="1.0" encoding="utf-8" ?>' +
  171. '<propfind xmlns="DAV:">' +
  172. '<prop>' +
  173. '<current-user-principal />' +
  174. '<displayname />' +
  175. '</prop>' +
  176. '</propfind>');
  177. return request;
  178. }
  179. /**
  180. * Find all calendars and addressbooks in collection.
  181. * @param {string} user
  182. * @param {string} password
  183. * @param {Collection} collection
  184. * @param {function(?Array<Collection>, ?string)} callback Returns result or error
  185. * @return {XMLHttpRequest}
  186. */
  187. function get_collections(user, password, collection, callback) {
  188. let request = new XMLHttpRequest();
  189. request.open("PROPFIND", SERVER + collection.href, true, user, encodeURIComponent(password));
  190. request.setRequestHeader("depth", "1");
  191. request.onreadystatechange = function() {
  192. if (request.readyState !== 4) {
  193. return;
  194. }
  195. if (request.status === 207) {
  196. let xml = request.responseXML;
  197. let collections = [];
  198. let response_query = "*|multistatus:root > *|response";
  199. let responses = xml.querySelectorAll(response_query);
  200. for (let i = 0; i < responses.length; i++) {
  201. let response = responses[i];
  202. let href_element = response.querySelector(response_query + " > *|href");
  203. let resourcetype_query = response_query + " > *|propstat > *|prop > *|resourcetype";
  204. let resourcetype_element = response.querySelector(resourcetype_query);
  205. let displayname_element = response.querySelector(response_query + " > *|propstat > *|prop > *|displayname");
  206. let calendarcolor_element = response.querySelector(response_query + " > *|propstat > *|prop > *|calendar-color");
  207. let addressbookcolor_element = response.querySelector(response_query + " > *|propstat > *|prop > *|addressbook-color");
  208. let calendardesc_element = response.querySelector(response_query + " > *|propstat > *|prop > *|calendar-description");
  209. let addressbookdesc_element = response.querySelector(response_query + " > *|propstat > *|prop > *|addressbook-description");
  210. let contentcount_element = response.querySelector(response_query + " > *|propstat > *|prop > *|getcontentcount");
  211. let contentlength_element = response.querySelector(response_query + " > *|propstat > *|prop > *|getcontentlength");
  212. let webcalsource_element = response.querySelector(response_query + " > *|propstat > *|prop > *|source");
  213. let components_query = response_query + " > *|propstat > *|prop > *|supported-calendar-component-set";
  214. let components_element = response.querySelector(components_query);
  215. let href = href_element ? href_element.textContent : "";
  216. let displayname = displayname_element ? displayname_element.textContent : "";
  217. let type = "";
  218. let color = "";
  219. let description = "";
  220. let source = "";
  221. let count = 0;
  222. let size = 0;
  223. if (resourcetype_element) {
  224. if (resourcetype_element.querySelector(resourcetype_query + " > *|addressbook")) {
  225. type = CollectionType.ADDRESSBOOK;
  226. color = addressbookcolor_element ? addressbookcolor_element.textContent : "";
  227. description = addressbookdesc_element ? addressbookdesc_element.textContent : "";
  228. count = contentcount_element ? parseInt(contentcount_element.textContent) : 0;
  229. size = contentlength_element ? parseInt(contentlength_element.textContent) : 0;
  230. } else if (resourcetype_element.querySelector(resourcetype_query + " > *|subscribed")) {
  231. type = CollectionType.WEBCAL;
  232. source = webcalsource_element ? webcalsource_element.textContent : "";
  233. color = calendarcolor_element ? calendarcolor_element.textContent : "";
  234. description = calendardesc_element ? calendardesc_element.textContent : "";
  235. } else if (resourcetype_element.querySelector(resourcetype_query + " > *|calendar")) {
  236. if (components_element) {
  237. if (components_element.querySelector(components_query + " > *|comp[name=VEVENT]")) {
  238. type = CollectionType.union(type, CollectionType.CALENDAR);
  239. }
  240. if (components_element.querySelector(components_query + " > *|comp[name=VJOURNAL]")) {
  241. type = CollectionType.union(type, CollectionType.JOURNAL);
  242. }
  243. if (components_element.querySelector(components_query + " > *|comp[name=VTODO]")) {
  244. type = CollectionType.union(type, CollectionType.TASKS);
  245. }
  246. }
  247. color = calendarcolor_element ? calendarcolor_element.textContent : "";
  248. description = calendardesc_element ? calendardesc_element.textContent : "";
  249. count = contentcount_element ? parseInt(contentcount_element.textContent) : 0;
  250. size = contentlength_element ? parseInt(contentlength_element.textContent) : 0;
  251. }
  252. }
  253. let sane_color = color.trim();
  254. if (sane_color) {
  255. let color_match = COLOR_RE.exec(sane_color);
  256. if (color_match) {
  257. sane_color = color_match[1];
  258. } else {
  259. sane_color = "";
  260. }
  261. }
  262. if (href.substr(-1) === "/" && href !== collection.href && type) {
  263. collections.push(new Collection(href, type, displayname, description, sane_color, count, size, source));
  264. }
  265. }
  266. collections.sort(function(a, b) {
  267. /** @type {string} */ let ca = a.displayname || a.href;
  268. /** @type {string} */ let cb = b.displayname || b.href;
  269. return ca.localeCompare(cb);
  270. });
  271. callback(collections, null);
  272. } else {
  273. callback(null, request.status + " " + request.statusText);
  274. }
  275. };
  276. request.send('<?xml version="1.0" encoding="utf-8" ?>' +
  277. '<propfind ' +
  278. 'xmlns="DAV:" ' +
  279. 'xmlns:C="urn:ietf:params:xml:ns:caldav" ' +
  280. 'xmlns:CR="urn:ietf:params:xml:ns:carddav" ' +
  281. 'xmlns:CS="http://calendarserver.org/ns/" ' +
  282. 'xmlns:I="http://apple.com/ns/ical/" ' +
  283. 'xmlns:INF="http://inf-it.com/ns/ab/" ' +
  284. 'xmlns:RADICALE="http://radicale.org/ns/"' +
  285. '>' +
  286. '<prop>' +
  287. '<resourcetype />' +
  288. '<RADICALE:displayname />' +
  289. '<I:calendar-color />' +
  290. '<INF:addressbook-color />' +
  291. '<C:calendar-description />' +
  292. '<C:supported-calendar-component-set />' +
  293. '<CR:addressbook-description />' +
  294. '<CS:source />' +
  295. '<RADICALE:getcontentcount />' +
  296. '<getcontentlength />' +
  297. '</prop>' +
  298. '</propfind>');
  299. return request;
  300. }
  301. /**
  302. * @param {string} user
  303. * @param {string} password
  304. * @param {string} collection_href Must always start and end with /.
  305. * @param {File} file
  306. * @param {function(?string)} callback Returns error or null
  307. * @return {XMLHttpRequest}
  308. */
  309. function upload_collection(user, password, collection_href, file, callback) {
  310. let request = new XMLHttpRequest();
  311. request.open("PUT", SERVER + collection_href, true, user, encodeURIComponent(password));
  312. request.onreadystatechange = function() {
  313. if (request.readyState !== 4) {
  314. return;
  315. }
  316. if (200 <= request.status && request.status < 300) {
  317. callback(null);
  318. } else {
  319. callback(request.status + " " + request.statusText);
  320. }
  321. };
  322. request.setRequestHeader("If-None-Match", "*");
  323. request.send(file);
  324. return request;
  325. }
  326. /**
  327. * @param {string} user
  328. * @param {string} password
  329. * @param {Collection} collection
  330. * @param {function(?string)} callback Returns error or null
  331. * @return {XMLHttpRequest}
  332. */
  333. function delete_collection(user, password, collection, callback) {
  334. let request = new XMLHttpRequest();
  335. request.open("DELETE", SERVER + collection.href, true, user, encodeURIComponent(password));
  336. request.onreadystatechange = function() {
  337. if (request.readyState !== 4) {
  338. return;
  339. }
  340. if (200 <= request.status && request.status < 300) {
  341. callback(null);
  342. } else {
  343. callback(request.status + " " + request.statusText);
  344. }
  345. };
  346. request.send();
  347. return request;
  348. }
  349. /**
  350. * @param {string} user
  351. * @param {string} password
  352. * @param {Collection} collection
  353. * @param {boolean} create
  354. * @param {function(?string)} callback Returns error or null
  355. * @return {XMLHttpRequest}
  356. */
  357. function create_edit_collection(user, password, collection, create, callback) {
  358. let request = new XMLHttpRequest();
  359. request.open(create ? "MKCOL" : "PROPPATCH", SERVER + collection.href, true, user, encodeURIComponent(password));
  360. request.onreadystatechange = function() {
  361. if (request.readyState !== 4) {
  362. return;
  363. }
  364. if (200 <= request.status && request.status < 300) {
  365. callback(null);
  366. } else {
  367. callback(request.status + " " + request.statusText);
  368. }
  369. };
  370. let displayname = escape_xml(collection.displayname);
  371. let calendar_color = "";
  372. let addressbook_color = "";
  373. let calendar_description = "";
  374. let addressbook_description = "";
  375. let calendar_source = "";
  376. let resourcetype;
  377. let components = "";
  378. if (collection.type === CollectionType.ADDRESSBOOK) {
  379. addressbook_color = escape_xml(collection.color + (collection.color ? "ff" : ""));
  380. addressbook_description = escape_xml(collection.description);
  381. resourcetype = '<CR:addressbook />';
  382. } else if (collection.type === CollectionType.WEBCAL) {
  383. calendar_color = escape_xml(collection.color + (collection.color ? "ff" : ""));
  384. calendar_description = escape_xml(collection.description);
  385. resourcetype = '<CS:subscribed />';
  386. calendar_source = escape_xml(collection.source);
  387. } else {
  388. calendar_color = escape_xml(collection.color + (collection.color ? "ff" : ""));
  389. calendar_description = escape_xml(collection.description);
  390. resourcetype = '<C:calendar />';
  391. if (CollectionType.is_subset(CollectionType.CALENDAR, collection.type)) {
  392. components += '<C:comp name="VEVENT" />';
  393. }
  394. if (CollectionType.is_subset(CollectionType.JOURNAL, collection.type)) {
  395. components += '<C:comp name="VJOURNAL" />';
  396. }
  397. if (CollectionType.is_subset(CollectionType.TASKS, collection.type)) {
  398. components += '<C:comp name="VTODO" />';
  399. }
  400. }
  401. let xml_request = create ? "mkcol" : "propertyupdate";
  402. request.send('<?xml version="1.0" encoding="UTF-8" ?>' +
  403. '<' + 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/">' +
  404. '<set>' +
  405. '<prop>' +
  406. (create ? '<resourcetype><collection />' + resourcetype + '</resourcetype>' : '') +
  407. (components ? '<C:supported-calendar-component-set>' + components + '</C:supported-calendar-component-set>' : '') +
  408. (displayname ? '<displayname>' + displayname + '</displayname>' : '') +
  409. (calendar_color ? '<I:calendar-color>' + calendar_color + '</I:calendar-color>' : '') +
  410. (addressbook_color ? '<INF:addressbook-color>' + addressbook_color + '</INF:addressbook-color>' : '') +
  411. (addressbook_description ? '<CR:addressbook-description>' + addressbook_description + '</CR:addressbook-description>' : '') +
  412. (calendar_description ? '<C:calendar-description>' + calendar_description + '</C:calendar-description>' : '') +
  413. (calendar_source ? '<CS:source>' + calendar_source + '</CS:source>' : '') +
  414. '</prop>' +
  415. '</set>' +
  416. (!create ? ('<remove>' +
  417. '<prop>' +
  418. (!components ? '<C:supported-calendar-component-set />' : '') +
  419. (!displayname ? '<displayname />' : '') +
  420. (!calendar_color ? '<I:calendar-color />' : '') +
  421. (!addressbook_color ? '<INF:addressbook-color />' : '') +
  422. (!addressbook_description ? '<CR:addressbook-description />' : '') +
  423. (!calendar_description ? '<C:calendar-description />' : '') +
  424. '</prop>' +
  425. '</remove>'): '') +
  426. '</' + xml_request + '>');
  427. return request;
  428. }
  429. /**
  430. * @param {string} user
  431. * @param {string} password
  432. * @param {Collection} collection
  433. * @param {function(?string)} callback Returns error or null
  434. * @return {XMLHttpRequest}
  435. */
  436. function create_collection(user, password, collection, callback) {
  437. return create_edit_collection(user, password, collection, true, callback);
  438. }
  439. /**
  440. * @param {string} user
  441. * @param {string} password
  442. * @param {Collection} collection
  443. * @param {function(?string)} callback Returns error or null
  444. * @return {XMLHttpRequest}
  445. */
  446. function edit_collection(user, password, collection, callback) {
  447. return create_edit_collection(user, password, collection, false, callback);
  448. }
  449. /**
  450. * @return {string}
  451. */
  452. function random_uuid() {
  453. return random_hex(8) + "-" + random_hex(4) + "-" + random_hex(4) + "-" + random_hex(4) + "-" + random_hex(12);
  454. }
  455. /**
  456. * @interface
  457. */
  458. function Scene() {}
  459. /**
  460. * Scene is on top of stack and visible.
  461. */
  462. Scene.prototype.show = function() {};
  463. /**
  464. * Scene is no longer visible.
  465. */
  466. Scene.prototype.hide = function() {};
  467. /**
  468. * Scene is removed from scene stack.
  469. */
  470. Scene.prototype.release = function() {};
  471. /**
  472. * @type {Array<Scene>}
  473. */
  474. let scene_stack = [];
  475. /**
  476. * Push scene onto stack.
  477. * @param {Scene} scene
  478. * @param {boolean} replace Replace the scene on top of the stack.
  479. */
  480. function push_scene(scene, replace) {
  481. if (scene_stack.length >= 1) {
  482. scene_stack[scene_stack.length - 1].hide();
  483. if (replace) {
  484. scene_stack.pop().release();
  485. }
  486. }
  487. scene_stack.push(scene);
  488. scene.show();
  489. }
  490. /**
  491. * Remove scenes from stack.
  492. * @param {number} index New top of stack
  493. */
  494. function pop_scene(index) {
  495. if (scene_stack.length - 1 <= index) {
  496. return;
  497. }
  498. scene_stack[scene_stack.length - 1].hide();
  499. while (scene_stack.length - 1 > index) {
  500. let old_length = scene_stack.length;
  501. scene_stack.pop().release();
  502. if (old_length - 1 === index + 1) {
  503. break;
  504. }
  505. }
  506. if (scene_stack.length >= 1) {
  507. let scene = scene_stack[scene_stack.length - 1];
  508. scene.show();
  509. } else {
  510. throw "Scene stack is empty";
  511. }
  512. }
  513. /**
  514. * @constructor
  515. * @implements {Scene}
  516. */
  517. function LoginScene() {
  518. let html_scene = document.getElementById("loginscene");
  519. let form = html_scene.querySelector("[data-name=form]");
  520. let user_form = html_scene.querySelector("[data-name=user]");
  521. let password_form = html_scene.querySelector("[data-name=password]");
  522. let error_form = html_scene.querySelector("[data-name=error]");
  523. let logout_view = document.getElementById("logoutview");
  524. let logout_user_form = logout_view.querySelector("[data-name=user]");
  525. let logout_btn = logout_view.querySelector("[data-name=logout]");
  526. let refresh_btn = logout_view.querySelector("[data-name=refresh]");
  527. /** @type {?number} */ let scene_index = null;
  528. let user = "";
  529. let error = "";
  530. /** @type {?XMLHttpRequest} */ let principal_req = null;
  531. function read_form() {
  532. user = user_form.value;
  533. }
  534. function fill_form() {
  535. user_form.value = user;
  536. password_form.value = "";
  537. if(error){
  538. error_form.textContent = "Error: " + error;
  539. error_form.classList.remove("hidden");
  540. }else{
  541. error_form.classList.add("hidden");
  542. }
  543. }
  544. function onlogin() {
  545. try {
  546. read_form();
  547. let password = password_form.value;
  548. if (user) {
  549. error = "";
  550. // setup logout
  551. logout_view.classList.remove("hidden");
  552. logout_btn.onclick = onlogout;
  553. refresh_btn.onclick = refresh;
  554. logout_user_form.textContent = user + "'s Collections";
  555. // Fetch principal
  556. let loading_scene = new LoadingScene();
  557. push_scene(loading_scene, false);
  558. principal_req = get_principal(user, password, function(collection, error1) {
  559. if (scene_index === null) {
  560. return;
  561. }
  562. principal_req = null;
  563. if (error1) {
  564. error = error1;
  565. pop_scene(scene_index);
  566. } else {
  567. // show collections
  568. let saved_user = user;
  569. user = "";
  570. let collections_scene = new CollectionsScene(
  571. saved_user, password, collection, function(error1) {
  572. error = error1;
  573. user = saved_user;
  574. });
  575. push_scene(collections_scene, true);
  576. }
  577. });
  578. } else {
  579. error = "Username is empty";
  580. fill_form();
  581. }
  582. } catch(err) {
  583. console.error(err);
  584. }
  585. return false;
  586. }
  587. function onlogout() {
  588. try {
  589. if (scene_index === null) {
  590. return false;
  591. }
  592. user = "";
  593. pop_scene(scene_index);
  594. } catch (err) {
  595. console.error(err);
  596. }
  597. return false;
  598. }
  599. function remove_logout() {
  600. logout_view.classList.add("hidden");
  601. logout_btn.onclick = null;
  602. refresh_btn.onclick = null;
  603. logout_user_form.textContent = "";
  604. }
  605. function refresh(){
  606. //The easiest way to refresh is to push a LoadingScene onto the stack and then pop it
  607. //forcing the scene below it, the Collections Scene to refresh itself.
  608. push_scene(new LoadingScene(), false);
  609. pop_scene(scene_stack.length-2);
  610. }
  611. this.show = function() {
  612. remove_logout();
  613. fill_form();
  614. form.onsubmit = onlogin;
  615. html_scene.classList.remove("hidden");
  616. scene_index = scene_stack.length - 1;
  617. user_form.focus();
  618. };
  619. this.hide = function() {
  620. read_form();
  621. html_scene.classList.add("hidden");
  622. form.onsubmit = null;
  623. };
  624. this.release = function() {
  625. scene_index = null;
  626. // cancel pending requests
  627. if (principal_req !== null) {
  628. principal_req.abort();
  629. principal_req = null;
  630. }
  631. remove_logout();
  632. };
  633. }
  634. /**
  635. * @constructor
  636. * @implements {Scene}
  637. */
  638. function LoadingScene() {
  639. let html_scene = document.getElementById("loadingscene");
  640. this.show = function() {
  641. html_scene.classList.remove("hidden");
  642. };
  643. this.hide = function() {
  644. html_scene.classList.add("hidden");
  645. };
  646. this.release = function() {};
  647. }
  648. /**
  649. * @constructor
  650. * @implements {Scene}
  651. * @param {string} user
  652. * @param {string} password
  653. * @param {Collection} collection The principal collection.
  654. * @param {function(string)} onerror Called when an error occurs, before the
  655. * scene is popped.
  656. */
  657. function CollectionsScene(user, password, collection, onerror) {
  658. let html_scene = document.getElementById("collectionsscene");
  659. let template = html_scene.querySelector("[data-name=collectiontemplate]");
  660. let new_btn = html_scene.querySelector("[data-name=new]");
  661. let upload_btn = html_scene.querySelector("[data-name=upload]");
  662. /** @type {?number} */ let scene_index = null;
  663. /** @type {?XMLHttpRequest} */ let collections_req = null;
  664. /** @type {?Array<Collection>} */ let collections = null;
  665. /** @type {Array<Node>} */ let nodes = [];
  666. function onnew() {
  667. try {
  668. let create_collection_scene = new CreateEditCollectionScene(user, password, collection);
  669. push_scene(create_collection_scene, false);
  670. } catch(err) {
  671. console.error(err);
  672. }
  673. return false;
  674. }
  675. function onupload() {
  676. try {
  677. let upload_scene = new UploadCollectionScene(user, password, collection);
  678. push_scene(upload_scene);
  679. } catch(err) {
  680. console.error(err);
  681. }
  682. return false;
  683. }
  684. function onedit(collection) {
  685. try {
  686. let edit_collection_scene = new CreateEditCollectionScene(user, password, collection);
  687. push_scene(edit_collection_scene, false);
  688. } catch(err) {
  689. console.error(err);
  690. }
  691. return false;
  692. }
  693. function ondelete(collection) {
  694. try {
  695. let delete_collection_scene = new DeleteCollectionScene(user, password, collection);
  696. push_scene(delete_collection_scene, false);
  697. } catch(err) {
  698. console.error(err);
  699. }
  700. return false;
  701. }
  702. function show_collections(collections) {
  703. let heightOfNavBar = document.querySelector("#logoutview").offsetHeight + "px";
  704. html_scene.style.marginTop = heightOfNavBar;
  705. html_scene.style.height = "calc(100vh - " + heightOfNavBar +")";
  706. collections.forEach(function (collection) {
  707. let node = template.cloneNode(true);
  708. node.classList.remove("hidden");
  709. let title_form = node.querySelector("[data-name=title]");
  710. let description_form = node.querySelector("[data-name=description]");
  711. let contentcount_form = node.querySelector("[data-name=contentcount]");
  712. let url_form = node.querySelector("[data-name=url]");
  713. let color_form = node.querySelector("[data-name=color]");
  714. let delete_btn = node.querySelector("[data-name=delete]");
  715. let edit_btn = node.querySelector("[data-name=edit]");
  716. let download_btn = node.querySelector("[data-name=download]");
  717. if (collection.color) {
  718. color_form.style.background = collection.color;
  719. }
  720. let possible_types = [CollectionType.ADDRESSBOOK, CollectionType.WEBCAL];
  721. [CollectionType.CALENDAR, ""].forEach(function(e) {
  722. [CollectionType.union(e, CollectionType.JOURNAL), e].forEach(function(e) {
  723. [CollectionType.union(e, CollectionType.TASKS), e].forEach(function(e) {
  724. if (e) {
  725. possible_types.push(e);
  726. }
  727. });
  728. });
  729. });
  730. possible_types.forEach(function(e) {
  731. if (e !== collection.type) {
  732. node.querySelector("[data-name=" + e + "]").classList.add("hidden");
  733. }
  734. });
  735. title_form.textContent = collection.displayname || collection.href;
  736. if(title_form.textContent.length > 30){
  737. title_form.classList.add("smalltext");
  738. }
  739. description_form.textContent = collection.description;
  740. if(description_form.textContent.length > 150){
  741. description_form.classList.add("smalltext");
  742. }
  743. if(collection.type != CollectionType.WEBCAL){
  744. let contentcount_form_txt = (collection.contentcount > 0 ? Number(collection.contentcount).toLocaleString() : "No") + " item" + (collection.contentcount == 1 ? "" : "s") + " in collection";
  745. if(collection.contentcount > 0){
  746. contentcount_form_txt += " (" + bytesToHumanReadable(collection.size) + ")";
  747. }
  748. contentcount_form.textContent = contentcount_form_txt;
  749. }
  750. let href = SERVER + collection.href;
  751. url_form.value = href;
  752. download_btn.href = href;
  753. if(collection.type == CollectionType.WEBCAL){
  754. download_btn.parentElement.classList.add("hidden");
  755. }
  756. delete_btn.onclick = function() {return ondelete(collection);};
  757. edit_btn.onclick = function() {return onedit(collection);};
  758. node.classList.remove("hidden");
  759. nodes.push(node);
  760. template.parentNode.insertBefore(node, template);
  761. });
  762. }
  763. function update() {
  764. let loading_scene = new LoadingScene();
  765. push_scene(loading_scene, false);
  766. collections_req = get_collections(user, password, collection, function(collections1, error) {
  767. if (scene_index === null) {
  768. return;
  769. }
  770. collections_req = null;
  771. if (error) {
  772. onerror(error);
  773. pop_scene(scene_index - 1);
  774. } else {
  775. collections = collections1;
  776. pop_scene(scene_index);
  777. }
  778. });
  779. }
  780. this.show = function() {
  781. html_scene.classList.remove("hidden");
  782. new_btn.onclick = onnew;
  783. upload_btn.onclick = onupload;
  784. if (collections === null) {
  785. update();
  786. } else {
  787. // from update loading scene
  788. show_collections(collections);
  789. }
  790. };
  791. this.hide = function() {
  792. html_scene.classList.add("hidden");
  793. scene_index = scene_stack.length - 1;
  794. new_btn.onclick = null;
  795. upload_btn.onclick = null;
  796. collections = null;
  797. // remove collection
  798. nodes.forEach(function(node) {
  799. node.parentNode.removeChild(node);
  800. });
  801. nodes = [];
  802. };
  803. this.release = function() {
  804. scene_index = null;
  805. if (collections_req !== null) {
  806. collections_req.abort();
  807. collections_req = null;
  808. }
  809. collections = null;
  810. };
  811. }
  812. /**
  813. * @constructor
  814. * @implements {Scene}
  815. * @param {string} user
  816. * @param {string} password
  817. * @param {Collection} collection parent collection
  818. * @param {Array<File>} files
  819. */
  820. function UploadCollectionScene(user, password, collection) {
  821. let html_scene = document.getElementById("uploadcollectionscene");
  822. let template = html_scene.querySelector("[data-name=filetemplate]");
  823. let upload_btn = html_scene.querySelector("[data-name=submit]");
  824. let close_btn = html_scene.querySelector("[data-name=close]");
  825. let uploadfile_form = html_scene.querySelector("[data-name=uploadfile]");
  826. let uploadfile_lbl = html_scene.querySelector("label[for=uploadfile]");
  827. let href_form = html_scene.querySelector("[data-name=href]");
  828. let href_label = html_scene.querySelector("label[for=href]");
  829. let hreflimitmsg_html = html_scene.querySelector("[data-name=hreflimitmsg]");
  830. let pending_html = html_scene.querySelector("[data-name=pending]");
  831. let files = uploadfile_form.files;
  832. href_form.addEventListener("keydown", cleanHREFinput);
  833. upload_btn.onclick = upload_start;
  834. uploadfile_form.onchange = onfileschange;
  835. href_form.value = "";
  836. /** @type {?number} */ let scene_index = null;
  837. /** @type {?XMLHttpRequest} */ let upload_req = null;
  838. /** @type {Array<string>} */ let results = [];
  839. /** @type {?Array<Node>} */ let nodes = null;
  840. function upload_start() {
  841. try {
  842. if(!read_form()){
  843. return false;
  844. }
  845. uploadfile_form.classList.add("hidden");
  846. uploadfile_lbl.classList.add("hidden");
  847. href_form.classList.add("hidden");
  848. href_label.classList.add("hidden");
  849. hreflimitmsg_html.classList.add("hidden");
  850. upload_btn.classList.add("hidden");
  851. close_btn.classList.add("hidden");
  852. pending_html.classList.remove("hidden");
  853. nodes = [];
  854. for (let i = 0; i < files.length; i++) {
  855. let file = files[i];
  856. let node = template.cloneNode(true);
  857. node.classList.remove("hidden");
  858. let name_form = node.querySelector("[data-name=name]");
  859. name_form.textContent = file.name;
  860. node.classList.remove("hidden");
  861. nodes.push(node);
  862. updateFileStatus(i);
  863. template.parentNode.insertBefore(node, template);
  864. }
  865. upload_next();
  866. } catch(err) {
  867. console.error(err);
  868. }
  869. return false;
  870. }
  871. function upload_next(){
  872. try{
  873. if (files.length === results.length) {
  874. pending_html.classList.add("hidden");
  875. close_btn.classList.remove("hidden");
  876. return;
  877. } else {
  878. let file = files[results.length];
  879. if(files.length > 1 || href.length == 0){
  880. href = random_uuid();
  881. }
  882. let upload_href = collection.href + href + "/";
  883. upload_req = upload_collection(user, password, upload_href, file, function(result) {
  884. upload_req = null;
  885. results.push(result);
  886. updateFileStatus(results.length - 1);
  887. upload_next();
  888. });
  889. }
  890. }catch(err){
  891. console.error(err);
  892. }
  893. }
  894. function onclose() {
  895. try {
  896. pop_scene(scene_index - 1);
  897. } catch(err) {
  898. console.error(err);
  899. }
  900. return false;
  901. }
  902. function updateFileStatus(i) {
  903. if (nodes === null) {
  904. return;
  905. }
  906. let success_form = nodes[i].querySelector("[data-name=success]");
  907. let error_form = nodes[i].querySelector("[data-name=error]");
  908. if (results.length > i) {
  909. if (results[i]) {
  910. success_form.classList.add("hidden");
  911. error_form.textContent = "Error: " + results[i];
  912. error_form.classList.remove("hidden");
  913. } else {
  914. success_form.classList.remove("hidden");
  915. error_form.classList.add("hidden");
  916. }
  917. } else {
  918. success_form.classList.add("hidden");
  919. error_form.classList.add("hidden");
  920. }
  921. }
  922. function read_form() {
  923. cleanHREFinput(href_form);
  924. let newhreftxtvalue = href_form.value.trim().toLowerCase();
  925. if(!isValidHREF(newhreftxtvalue)){
  926. alert("You must enter a valid HREF");
  927. return false;
  928. }
  929. href = newhreftxtvalue;
  930. if(uploadfile_form.files.length == 0){
  931. alert("You must select at least one file to upload");
  932. return false;
  933. }
  934. files = uploadfile_form.files;
  935. return true;
  936. }
  937. function onfileschange() {
  938. files = uploadfile_form.files;
  939. if(files.length > 1){
  940. hreflimitmsg_html.classList.remove("hidden");
  941. href_form.classList.add("hidden");
  942. href_label.classList.add("hidden");
  943. href_form.value = random_uuid(); // dummy, will be replaced on upload
  944. }else{
  945. hreflimitmsg_html.classList.add("hidden");
  946. href_form.classList.remove("hidden");
  947. href_label.classList.remove("hidden");
  948. href_form.value = files[0].name.replace(/\.(ics|vcf)$/, '');
  949. }
  950. return false;
  951. }
  952. this.show = function() {
  953. scene_index = scene_stack.length - 1;
  954. html_scene.classList.remove("hidden");
  955. close_btn.onclick = onclose;
  956. if(error){
  957. error_form.textContent = "Error: " + error;
  958. error_form.classList.remove("hidden");
  959. }else{
  960. error_form.classList.add("hidden");
  961. }
  962. };
  963. this.hide = function() {
  964. html_scene.classList.add("hidden");
  965. close_btn.classList.remove("hidden");
  966. upload_btn.classList.remove("hidden");
  967. uploadfile_form.classList.remove("hidden");
  968. uploadfile_lbl.classList.remove("hidden");
  969. href_form.classList.remove("hidden");
  970. href_label.classList.remove("hidden");
  971. hreflimitmsg_html.classList.add("hidden");
  972. pending_html.classList.add("hidden");
  973. close_btn.onclick = null;
  974. upload_btn.onclick = null;
  975. href_form.value = "";
  976. uploadfile_form.value = "";
  977. if(nodes == null){
  978. return;
  979. }
  980. nodes.forEach(function(node) {
  981. node.parentNode.removeChild(node);
  982. });
  983. nodes = null;
  984. };
  985. this.release = function() {
  986. scene_index = null;
  987. if (upload_req !== null) {
  988. upload_req.abort();
  989. upload_req = null;
  990. }
  991. };
  992. }
  993. /**
  994. * @constructor
  995. * @implements {Scene}
  996. * @param {string} user
  997. * @param {string} password
  998. * @param {Collection} collection
  999. */
  1000. function DeleteCollectionScene(user, password, collection) {
  1001. let html_scene = document.getElementById("deletecollectionscene");
  1002. let title_form = html_scene.querySelector("[data-name=title]");
  1003. let error_form = html_scene.querySelector("[data-name=error]");
  1004. let confirmation_txt = html_scene.querySelector("[data-name=confirmationtxt]");
  1005. let delete_confirmation_lbl = html_scene.querySelector("[data-name=deleteconfirmationtext]");
  1006. let delete_btn = html_scene.querySelector("[data-name=delete]");
  1007. let cancel_btn = html_scene.querySelector("[data-name=cancel]");
  1008. delete_confirmation_lbl.innerHTML = DELETE_CONFIRMATION_TEXT;
  1009. confirmation_txt.value = "";
  1010. confirmation_txt.addEventListener("keydown", onkeydown);
  1011. /** @type {?number} */ let scene_index = null;
  1012. /** @type {?XMLHttpRequest} */ let delete_req = null;
  1013. let error = "";
  1014. function ondelete() {
  1015. let confirmation_text_value = confirmation_txt.value;
  1016. if(confirmation_text_value != DELETE_CONFIRMATION_TEXT){
  1017. alert("Please type the confirmation text to delete this collection.");
  1018. return;
  1019. }
  1020. try {
  1021. let loading_scene = new LoadingScene();
  1022. push_scene(loading_scene);
  1023. delete_req = delete_collection(user, password, collection, function(error1) {
  1024. if (scene_index === null) {
  1025. return;
  1026. }
  1027. delete_req = null;
  1028. if (error1) {
  1029. error = error1;
  1030. pop_scene(scene_index);
  1031. } else {
  1032. pop_scene(scene_index - 1);
  1033. }
  1034. });
  1035. } catch(err) {
  1036. console.error(err);
  1037. }
  1038. return false;
  1039. }
  1040. function oncancel() {
  1041. try {
  1042. pop_scene(scene_index - 1);
  1043. } catch(err) {
  1044. console.error(err);
  1045. }
  1046. return false;
  1047. }
  1048. function onkeydown(event){
  1049. if (event.keyCode !== 13) {
  1050. return;
  1051. }
  1052. ondelete();
  1053. }
  1054. this.show = function() {
  1055. this.release();
  1056. scene_index = scene_stack.length - 1;
  1057. html_scene.classList.remove("hidden");
  1058. title_form.textContent = collection.displayname || collection.href;
  1059. delete_btn.onclick = ondelete;
  1060. cancel_btn.onclick = oncancel;
  1061. if(error){
  1062. error_form.textContent = "Error: " + error;
  1063. error_form.classList.remove("hidden");
  1064. }else{
  1065. error_form.classList.add("hidden");
  1066. }
  1067. };
  1068. this.hide = function() {
  1069. html_scene.classList.add("hidden");
  1070. cancel_btn.onclick = null;
  1071. delete_btn.onclick = null;
  1072. };
  1073. this.release = function() {
  1074. scene_index = null;
  1075. if (delete_req !== null) {
  1076. delete_req.abort();
  1077. delete_req = null;
  1078. }
  1079. };
  1080. }
  1081. /**
  1082. * Generate random hex number.
  1083. * @param {number} length
  1084. * @return {string}
  1085. */
  1086. function random_hex(length) {
  1087. let bytes = new Uint8Array(Math.ceil(length / 2));
  1088. window.crypto.getRandomValues(bytes);
  1089. return bytes.reduce((s, b) => s + b.toString(16).padStart(2, "0"), "").substring(0, length);
  1090. }
  1091. /**
  1092. * @constructor
  1093. * @implements {Scene}
  1094. * @param {string} user
  1095. * @param {string} password
  1096. * @param {Collection} collection if it's a principal collection, a new
  1097. * collection will be created inside of it.
  1098. * Otherwise the collection will be edited.
  1099. */
  1100. function CreateEditCollectionScene(user, password, collection) {
  1101. let edit = collection.type !== CollectionType.PRINCIPAL;
  1102. let html_scene = document.getElementById(edit ? "editcollectionscene" : "createcollectionscene");
  1103. let title_form = edit ? html_scene.querySelector("[data-name=title]") : null;
  1104. let error_form = html_scene.querySelector("[data-name=error]");
  1105. let href_form = html_scene.querySelector("[data-name=href]");
  1106. let href_label = html_scene.querySelector("label[for=href]");
  1107. let displayname_form = html_scene.querySelector("[data-name=displayname]");
  1108. let displayname_label = html_scene.querySelector("label[for=displayname]");
  1109. let description_form = html_scene.querySelector("[data-name=description]");
  1110. let description_label = html_scene.querySelector("label[for=description]");
  1111. let source_form = html_scene.querySelector("[data-name=source]");
  1112. let source_label = html_scene.querySelector("label[for=source]");
  1113. let type_form = html_scene.querySelector("[data-name=type]");
  1114. let type_label = html_scene.querySelector("label[for=type]");
  1115. let color_form = html_scene.querySelector("[data-name=color]");
  1116. let color_label = html_scene.querySelector("label[for=color]");
  1117. let submit_btn = html_scene.querySelector("[data-name=submit]");
  1118. let cancel_btn = html_scene.querySelector("[data-name=cancel]");
  1119. /** @type {?number} */ let scene_index = null;
  1120. /** @type {?XMLHttpRequest} */ let create_edit_req = null;
  1121. let error = "";
  1122. /** @type {?Element} */ let saved_type_form = null;
  1123. let href = edit ? collection.href : collection.href + random_uuid() + "/";
  1124. let displayname = edit ? collection.displayname : "";
  1125. let description = edit ? collection.description : "";
  1126. let source = edit ? collection.source : "";
  1127. let type = edit ? collection.type : CollectionType.CALENDAR_JOURNAL_TASKS;
  1128. let color = edit && collection.color ? collection.color : "#" + random_hex(6);
  1129. if(!edit){
  1130. href_form.addEventListener("keydown", cleanHREFinput);
  1131. }
  1132. function remove_invalid_types() {
  1133. if (!edit) {
  1134. return;
  1135. }
  1136. /** @type {HTMLOptionsCollection} */ let options = type_form.options;
  1137. // remove all options that are not supersets
  1138. let valid_type_options = CollectionType.valid_options_for_type(type);
  1139. for (let i = options.length - 1; i >= 0; i--) {
  1140. if (valid_type_options.indexOf(options[i].value) < 0) {
  1141. options.remove(i);
  1142. }
  1143. }
  1144. }
  1145. function read_form() {
  1146. if(!edit){
  1147. cleanHREFinput(href_form);
  1148. let newhreftxtvalue = href_form.value.trim().toLowerCase();
  1149. if(!isValidHREF(newhreftxtvalue)){
  1150. alert("You must enter a valid HREF");
  1151. return false;
  1152. }
  1153. href = collection.href + newhreftxtvalue + "/";
  1154. }
  1155. displayname = displayname_form.value;
  1156. description = description_form.value;
  1157. source = source_form.value;
  1158. type = type_form.value;
  1159. color = color_form.value;
  1160. return true;
  1161. }
  1162. function fill_form() {
  1163. if(!edit){
  1164. href_form.value = random_uuid();
  1165. }
  1166. displayname_form.value = displayname;
  1167. description_form.value = description;
  1168. source_form.value = source;
  1169. type_form.value = type;
  1170. color_form.value = color;
  1171. if(error){
  1172. error_form.textContent = "Error: " + error;
  1173. error_form.classList.remove("hidden");
  1174. }
  1175. error_form.classList.add("hidden");
  1176. onTypeChange();
  1177. type_form.addEventListener("change", onTypeChange);
  1178. }
  1179. function onsubmit() {
  1180. try {
  1181. if(!read_form()){
  1182. return false;
  1183. }
  1184. let sane_color = color.trim();
  1185. if (sane_color) {
  1186. let color_match = COLOR_RE.exec(sane_color);
  1187. if (!color_match) {
  1188. error = "Invalid color";
  1189. fill_form();
  1190. return false;
  1191. }
  1192. sane_color = color_match[1];
  1193. }
  1194. let loading_scene = new LoadingScene();
  1195. push_scene(loading_scene);
  1196. let collection = new Collection(href, type, displayname, description, sane_color, 0, 0, source);
  1197. let callback = function(error1) {
  1198. if (scene_index === null) {
  1199. return;
  1200. }
  1201. create_edit_req = null;
  1202. if (error1) {
  1203. error = error1;
  1204. pop_scene(scene_index);
  1205. } else {
  1206. pop_scene(scene_index - 1);
  1207. }
  1208. };
  1209. if (edit) {
  1210. create_edit_req = edit_collection(user, password, collection, callback);
  1211. } else {
  1212. create_edit_req = create_collection(user, password, collection, callback);
  1213. }
  1214. } catch(err) {
  1215. console.error(err);
  1216. }
  1217. return false;
  1218. }
  1219. function oncancel() {
  1220. try {
  1221. pop_scene(scene_index - 1);
  1222. } catch(err) {
  1223. console.error(err);
  1224. }
  1225. return false;
  1226. }
  1227. function onTypeChange(e){
  1228. if(type_form.value == CollectionType.WEBCAL){
  1229. source_label.classList.remove("hidden");
  1230. source_form.classList.remove("hidden");
  1231. }else{
  1232. source_label.classList.add("hidden");
  1233. source_form.classList.add("hidden");
  1234. }
  1235. }
  1236. this.show = function() {
  1237. this.release();
  1238. scene_index = scene_stack.length - 1;
  1239. // Clone type_form because it's impossible to hide options without removing them
  1240. saved_type_form = type_form;
  1241. type_form = type_form.cloneNode(true);
  1242. saved_type_form.parentNode.replaceChild(type_form, saved_type_form);
  1243. remove_invalid_types();
  1244. html_scene.classList.remove("hidden");
  1245. if (edit) {
  1246. title_form.textContent = collection.displayname || collection.href;
  1247. }
  1248. fill_form();
  1249. submit_btn.onclick = onsubmit;
  1250. cancel_btn.onclick = oncancel;
  1251. if(error){
  1252. error_form.textContent = "Error: " + error;
  1253. error_form.classList.remove("hidden");
  1254. }else{
  1255. error_form.classList.add("hidden");
  1256. }
  1257. };
  1258. this.hide = function() {
  1259. read_form();
  1260. html_scene.classList.add("hidden");
  1261. // restore type_form
  1262. type_form.parentNode.replaceChild(saved_type_form, type_form);
  1263. type_form = saved_type_form;
  1264. saved_type_form = null;
  1265. submit_btn.onclick = null;
  1266. cancel_btn.onclick = null;
  1267. };
  1268. this.release = function() {
  1269. scene_index = null;
  1270. if (create_edit_req !== null) {
  1271. create_edit_req.abort();
  1272. create_edit_req = null;
  1273. }
  1274. };
  1275. }
  1276. /**
  1277. * Removed invalid HREF characters for a collection HREF.
  1278. *
  1279. * @param a A valid Input element or an onchange Event of an Input element.
  1280. */
  1281. function cleanHREFinput(a) {
  1282. let href_form = a;
  1283. if (a.target) {
  1284. href_form = a.target;
  1285. }
  1286. let currentTxtVal = href_form.value.trim().toLowerCase();
  1287. //Clean the HREF to remove not permitted chars
  1288. currentTxtVal = currentTxtVal.replace(/(?![0-9a-z\-\_\.])./g, '');
  1289. //Clean the HREF to remove leading . (would result in hidden directory)
  1290. currentTxtVal = currentTxtVal.replace(/^\./, '');
  1291. href_form.value = currentTxtVal;
  1292. }
  1293. /**
  1294. * Checks if a proposed HREF for a collection has a valid format and syntax.
  1295. *
  1296. * @param href String of the porposed HREF.
  1297. *
  1298. * @return Boolean results if the HREF is valid.
  1299. */
  1300. function isValidHREF(href) {
  1301. if (href.length < 1) {
  1302. return false;
  1303. }
  1304. if (href.indexOf("/") != -1) {
  1305. return false;
  1306. }
  1307. return true;
  1308. }
  1309. /**
  1310. * Format bytes to human-readable text.
  1311. *
  1312. * @param bytes Number of bytes.
  1313. *
  1314. * @return Formatted string.
  1315. */
  1316. function bytesToHumanReadable(bytes, dp=1) {
  1317. let isNumber = !isNaN(parseFloat(bytes)) && !isNaN(bytes - 0);
  1318. if(!isNumber){
  1319. return "";
  1320. }
  1321. var i = bytes == 0 ? 0 : Math.floor(Math.log(bytes) / Math.log(1024));
  1322. return (bytes / Math.pow(1024, i)).toFixed(dp) * 1 + ' ' + ['b', 'kb', 'mb', 'gb', 'tb'][i];
  1323. }
  1324. function main() {
  1325. // Hide startup loading message
  1326. document.getElementById("loadingscene").classList.add("hidden");
  1327. push_scene(new LoginScene(), false);
  1328. }
  1329. window.addEventListener("load", main);