caldav-client

standalone CalDAV web client
git clone https://git.ce9e.org/caldav-client.git

commit
9c4b3e5c86a95809470955a9632ba405aa716331
parent
23b4192227f944b704dbcfe0e08248a9b9123722
Author
Tobias Bengfort <tobias.bengfort@posteo.de>
Date
2022-02-04 17:01
integrate with ical

Diffstat

A dav.js 174 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
M index.html 1 +
M main.js 18 ++++++++++++++++++
M package.json 3 ++-

4 files changed, 195 insertions, 1 deletions


diff --git a/dav.js b/dav.js

@@ -0,0 +1,174 @@
   -1     1 var uuid = function() {
   -1     2     if (crypto.randomUUID) {
   -1     3         return crypto.randomUUID();
   -1     4     }
   -1     5 
   -1     6     var rnds = new Uint8Array(16);
   -1     7     crypto.getRandomValues(rnds);
   -1     8     rnds[6] = (rnds[6] & 0x0f) | 0x40;
   -1     9     rnds[8] = (rnds[8] & 0x3f) | 0x80;
   -1    10 
   -1    11     var s = '';
   -1    12     for (var i = 0; i < 16; i++) {
   -1    13         s += (rnds[i] + 0x100).toString(16).substr(1);
   -1    14         if (i == 3 || i == 5 || i == 7 || i == 9) {
   -1    15             s += '-';
   -1    16         }
   -1    17     }
   -1    18     return s;
   -1    19 };
   -1    20 
   -1    21 var date2idate = function(date, allDay, offset) {
   -1    22     var odate = offset ? new Date(date - offset) : date;
   -1    23     var idate = ICAL.Time.fromJSDate(odate);
   -1    24     if (allDay) {
   -1    25         idate.hour = 0;
   -1    26         idate.minute = 0;
   -1    27         idate.second = 0;
   -1    28         idate.isDate = true;
   -1    29     }
   -1    30     return idate;
   -1    31 };
   -1    32 
   -1    33 var formatDate = function(date) {
   -1    34     return date
   -1    35         .toISOString()
   -1    36         .replace(/[-:.]/g, '')
   -1    37         .replace('000Z', 'Z');
   -1    38 };
   -1    39 
   -1    40 export var getCalendars = function(url) {
   -1    41     return fetch(url, {
   -1    42         method: 'PROPFIND',
   -1    43         credentials: 'same-origin',
   -1    44         body: '<?xml version="1.0" encoding="utf-8"?>\n'
   -1    45             + '<D:propfind xmlns:D="DAV:">\n'
   -1    46             + '  <D:prop>\n'
   -1    47             + '    <D:resourcetype/>\n'
   -1    48             + '    <D:displayname/>\n'
   -1    49             + '    <A:calendar-color xmlns:A="http://apple.com/ns/ical/"/>\n'
   -1    50             + '  </D:prop>\n'
   -1    51             + '</D:propfind>',
   -1    52     }).then(function(response) {
   -1    53         if (response.ok) {
   -1    54             return response.text();
   -1    55         } else {
   -1    56             throw response;
   -1    57         }
   -1    58     }).then(function(xml) {
   -1    59         var parser = new DOMParser();
   -1    60         var dom = parser.parseFromString(xml, 'text/xml');
   -1    61         var calendars = [];
   -1    62         dom.querySelectorAll('response').forEach(response => {
   -1    63             if (response.querySelector('resourcetype calendar')) {
   -1    64                 calendars.push({
   -1    65                     href: response.querySelector('href').textContent,
   -1    66                     name: response.querySelector('displayname').textContent,
   -1    67                     color: response.querySelector('calendar-color').textContent,
   -1    68                 });
   -1    69             }
   -1    70         });
   -1    71         return calendars;
   -1    72     });
   -1    73 };
   -1    74 
   -1    75 export var getEvents = function(href, info) {
   -1    76     return fetch(href, {
   -1    77         method: 'REPORT',
   -1    78         credentials: 'same-origin',
   -1    79         headers: {depth: '1'},
   -1    80         body: '<?xml version="1.0" encoding="utf-8"?>\n'
   -1    81             + '<L:calendar-query xmlns:L="urn:ietf:params:xml:ns:caldav">\n'
   -1    82             + '  <D:prop xmlns:D="DAV:">\n'
   -1    83             + '    <D:getcontenttype/>\n'
   -1    84             + '    <D:getetag/>\n'
   -1    85             + '    <L:calendar-data/>\n'
   -1    86             + '  </D:prop>\n'
   -1    87             + '  <L:filter>\n'
   -1    88             + '    <L:comp-filter name="VCALENDAR">\n'
   -1    89             + '      <L:comp-filter name="VEVENT">\n'
   -1    90             + `        <L:time-range start="${formatDate(info.start)}" end="${formatDate(info.end)}"/>\n`
   -1    91             + '      </L:comp-filter>\n'
   -1    92             + '    </L:comp-filter>\n'
   -1    93             + '  </L:filter>\n'
   -1    94             + '</L:calendar-query>',
   -1    95     }).then(function(response) {
   -1    96         if (response.ok) {
   -1    97             return response.text();
   -1    98         } else {
   -1    99             throw response;
   -1   100         }
   -1   101     }).then(function(xml) {
   -1   102         var parser = new DOMParser();
   -1   103         var dom = parser.parseFromString(xml, 'text/xml');
   -1   104         var events = [];
   -1   105         dom.querySelectorAll('response').forEach(response => {
   -1   106             // https://github.com/mozilla-comm/ical.js/wiki
   -1   107             var ics = response.querySelector('calendar-data').textContent;
   -1   108             var jcal = ICAL.parse(ics);
   -1   109             var comp = new ICAL.Component(jcal);
   -1   110             var vevent = new ICAL.Event(comp.getFirstSubcomponent('vevent'));
   -1   111             var iter = vevent.iterator();
   -1   112             var start = vevent.startDate.toJSDate();
   -1   113             var end = vevent.endDate.toJSDate();
   -1   114             var i;
   -1   115             while (i = iter.next()) {
   -1   116                 var istart = i.toJSDate();
   -1   117                 if (istart < info.start) {
   -1   118                     continue;
   -1   119                 } else if (istart > info.end) {
   -1   120                     break;
   -1   121                 }
   -1   122                 events.push({
   -1   123                     groupId: response.querySelector('href').textContent,
   -1   124                     title: vevent.summary,
   -1   125                     offset: istart - start,
   -1   126                     start: istart,
   -1   127                     end: new Date(istart - (start - end)),
   -1   128                     allDay: vevent.startDate.isDate,
   -1   129                     comp: comp,
   -1   130                 });
   -1   131             }
   -1   132         });
   -1   133         return events;
   -1   134     });
   -1   135 };
   -1   136 
   -1   137 export var createEvent = function(info, source) {
   -1   138     var comp = new ICAL.Component(['vcalendar', [], []]);
   -1   139     var compEvent = new ICAL.Component('vevent');
   -1   140     comp.updatePropertyWithValue('prodid', '-//iCal.js Wiki Example');
   -1   141     comp.addSubcomponent(compEvent);
   -1   142 
   -1   143     var vevent = new ICAL.Event(compEvent);
   -1   144     vevent.uid = uuid();
   -1   145 
   -1   146     return {
   -1   147         groupId: source.id + vevent.uid + '.ics',  // FIXME: assumptions about href structure
   -1   148         title: 'new event',
   -1   149         offset: 0,
   -1   150         start: info.date,
   -1   151         allDay: info.allDay,
   -1   152         comp: comp,
   -1   153     };
   -1   154 };
   -1   155 
   -1   156 export var commitEvent = function(data) {
   -1   157     var comp = data.extendedProps.comp;
   -1   158     var vevent = new ICAL.Event(comp.getFirstSubcomponent('vevent'));
   -1   159     vevent.summary = data.title;
   -1   160     vevent.startDate = date2idate(data.start, data.allDay, data.extendedProps.offset);
   -1   161     vevent.endDate = date2idate(data.end || data.start, data.allDay, data.extendedProps.offset);
   -1   162     return fetch(data.groupId, {
   -1   163         method: 'PUT',
   -1   164         credentials: 'same-origin',
   -1   165         body: comp.toString(),
   -1   166     });
   -1   167 };
   -1   168 
   -1   169 export var deleteEvent = function(data) {
   -1   170     return fetch(data.groupId, {
   -1   171         method: 'DELETE',
   -1   172         credentials: 'same-origin',
   -1   173     });
   -1   174 };

diff --git a/index.html b/index.html

@@ -12,6 +12,7 @@
   12    12 
   13    13     <script src="node_modules/fullcalendar-scheduler/main.js"></script>
   14    14     <script src="node_modules/fullcalendar-scheduler/locales/de.js"></script>
   -1    15     <script src="node_modules/ical.js/build/ical.js"></script>
   15    16     <script src="main.js" type="module"></script>
   16    17 </body>
   17    18 </html>

diff --git a/main.js b/main.js

@@ -1,3 +1,6 @@
   -1     1 import * as config from './config.js';
   -1     2 import * as dav from './dav.js';
   -1     3 
    1     4 var calendar = new FullCalendar.Calendar(
    2     5     document.querySelector('.calendar'),
    3     6     {
@@ -6,6 +9,8 @@ var calendar = new FullCalendar.Calendar(
    6     9         scrollTime: '07:00',
    7    10         nowIndicator: true,
    8    11         weekNumberCalculation: 'ISO',
   -1    12         eventDrop: info => dav.commitEvent(info.event),
   -1    13         eventResize: info => dav.commitEvent(info.event),
    9    14         height: '100%',
   10    15         headerToolbar: {
   11    16             left: 'timeGridWeek,dayGridMonth',
@@ -16,3 +21,16 @@ var calendar = new FullCalendar.Calendar(
   16    21     }
   17    22 );
   18    23 calendar.render();
   -1    24 
   -1    25 dav.getCalendars(config.rootUrl).then(calendars => {
   -1    26     calendars.forEach(cal => {
   -1    27         calendar.addEventSource({
   -1    28             id: cal.href,
   -1    29             color: cal.color,
   -1    30             editable: true,
   -1    31             events: function(info, success, error) {
   -1    32                 dav.getEvents(cal.href, info).then(success, error);
   -1    33             },
   -1    34         });
   -1    35     });
   -1    36 });

diff --git a/package.json b/package.json

@@ -5,6 +5,7 @@
    5     5   "author": "Tobias Bengfort <tobias.bengfort@posteo.de>",
    6     6   "license": "MIT",
    7     7   "dependencies": {
    8    -1     "fullcalendar-scheduler": "^5.10.1"
   -1     8     "fullcalendar-scheduler": "^5.10.1",
   -1     9     "ical.js": "^1.4.0"
    9    10   }
   10    11 }