diff options
-rw-r--r-- | module/calp/entry-points/server.scm | 2 | ||||
-rw-r--r-- | module/calp/html/vcomponent.scm | 24 | ||||
-rw-r--r-- | module/calp/html/view/calendar.scm | 1 | ||||
-rw-r--r-- | module/calp/server/routes.scm | 5 | ||||
-rw-r--r-- | module/vcomponent/xcal/parse.scm | 5 | ||||
-rw-r--r-- | static/input_list.js | 6 | ||||
-rw-r--r-- | static/jcal-tests.js | 32 | ||||
-rw-r--r-- | static/jcal.js | 174 | ||||
-rw-r--r-- | static/lib.js | 14 | ||||
-rw-r--r-- | static/recur.js | 0 | ||||
-rw-r--r-- | static/rrule.js | 39 | ||||
-rw-r--r-- | static/script.js | 71 | ||||
-rw-r--r-- | static/server_connect.js | 84 | ||||
-rw-r--r-- | static/style.scss | 8 | ||||
-rw-r--r-- | static/types.js | 11 |
15 files changed, 388 insertions, 88 deletions
diff --git a/module/calp/entry-points/server.scm b/module/calp/entry-points/server.scm index 55f84c1a..a456c292 100644 --- a/module/calp/entry-points/server.scm +++ b/module/calp/entry-points/server.scm @@ -78,7 +78,7 @@ (catch 'system-error (lambda () - (start-server `(family: ,family port: ,port host: ,addr))) + (start-server (list family: family port: port host: addr))) ;; probably address already in use (lambda (err proc fmt args errno) diff --git a/module/calp/html/vcomponent.scm b/module/calp/html/vcomponent.scm index 5d10c996..cd8c207e 100644 --- a/module/calp/html/vcomponent.scm +++ b/module/calp/html/vcomponent.scm @@ -7,7 +7,7 @@ :use-module ((text util) :select (add-enumeration-punctuation)) :use-module ((web uri-query) :select (encode-query-parameters)) :use-module (calp html util) - :use-module ((calp html config) :select (edit-mode)) + :use-module ((calp html config) :select (edit-mode debug)) :use-module ((calp html components) :select (btn tabset form with-label)) :use-module ((calp util color) :select (calculate-fg-color)) :use-module ((vcomponent recurrence internal) :prefix #{rrule:}#) @@ -371,6 +371,10 @@ (define (editable-repeat-info event) `(div (@ (class "eventtext")) (h2 "Upprepningar") + ,@(when (debug) + '((button (@ (style "position:absolute;right:1ex;top:1ex") + (onclick "console.log(event_from_popup(this.closest('.popup-container')).properties.rrule.asJcal());")) + "js"))) (table (@ (class "recur-components bind") (name "rrule") (data-bindby "bind_recur")) @@ -531,10 +535,10 @@ (list (btn "🖊️" title: "Redigera" - onclick: "place_in_edit_mode(document.getElementById(this.closest('.popup-container').id.substr(5)))") + onclick: "place_in_edit_mode(event_from_popup(this.closest('.popup-container')))") (btn "🗑" title: "Ta bort" - onclick: "remove_event(document.getElementById(this.closest('.popup-container').id.substr(5)))")))) + onclick: "remove_event(event_from_popup(this.closest('.popup-container')))")))) ,(tabset `(("📅" title: "Översikt" @@ -546,10 +550,16 @@ ("⤓" title: "Nedladdning" (div (@ (class "eventtext") (style "font-family:sans")) (h2 "Ladda ner") - (ul (li (a (@ (href "/calendar/" ,(prop ev 'UID) ".ics")) - "som iCal")) - (li (a (@ (href "/calendar/" ,(prop ev 'UID) ".xcs")) - "som xCal"))))) + (div (@ (class "side-by-side")) + (ul (li (a (@ (href "/calendar/" ,(prop ev 'UID) ".ics")) + "som iCal")) + (li (a (@ (href "/calendar/" ,(prop ev 'UID) ".xcs")) + "som xCal"))) + ,@(when (debug) + `((ul + (li (button (@ (onclick "console.log(event_to_jcal(event_from_popup(this.closest('.popup-container'))));")) "js")) + (li (button (@ (onclick "console.log(jcal_to_xcal(event_to_jcal(event_from_popup(this.closest('.popup-container')))));")) "xml")))))) + )) ,@(when (prop ev 'RRULE) `(("↺" title: "Upprepningar" class: "repeating" diff --git a/module/calp/html/view/calendar.scm b/module/calp/html/view/calendar.scm index f66e347c..3f607bb7 100644 --- a/module/calp/html/view/calendar.scm +++ b/module/calp/html/view/calendar.scm @@ -106,6 +106,7 @@ (script (@ (defer) (src "/static/types.js"))) (script (@ (defer) (src "/static/lib.js"))) + (script (@ (defer) (src "/static/jcal.js"))) (script (@ (defer) (src "/static/dragable.js"))) (script (@ (defer) (src "/static/clock.js"))) (script (@ (defer) (src "/static/popup.js"))) diff --git a/module/calp/server/routes.scm b/module/calp/server/routes.scm index 276513f5..16ab2662 100644 --- a/module/calp/server/routes.scm +++ b/module/calp/server/routes.scm @@ -185,6 +185,8 @@ ;; (vcalendar ;; (vevent ...)))) ;; @end example + + ;; TODO ;; However, *PI* will probably be omited, and currently events ;; are sent without the vcalendar part. Earlier versions ;; Also omitted the icalendar part. And I'm not sure if the @@ -197,7 +199,8 @@ (move-to-namespace ;; TODO Multiple event components (car ((sxpath '(// IC:vevent)) - (xml->sxml data namespaces: '((IC . "urn:ietf:params:xml:ns:icalendar-2.0"))))) + (xml->sxml data namespaces: + '((IC . "urn:ietf:params:xml:ns:icalendar-2.0"))))) #f)) (lambda (err port . args) (return (build-response code: 400) diff --git a/module/vcomponent/xcal/parse.scm b/module/vcomponent/xcal/parse.scm index 6b877b9f..6b752874 100644 --- a/module/vcomponent/xcal/parse.scm +++ b/module/vcomponent/xcal/parse.scm @@ -50,8 +50,9 @@ [(recur) (apply (@ (vcomponent recurrence internal) make-recur-rule) - (for (k v) in value - (list (symbol->keyword k) v)))] + (concatenate + (for (k v) in value + (list (symbol->keyword k) v))))] [(time) (parse-iso-time (car value))] diff --git a/static/input_list.js b/static/input_list.js index 7cfbc080..a7a446f3 100644 --- a/static/input_list.js +++ b/static/input_list.js @@ -77,7 +77,7 @@ function init_input_list() { if (lst.dataset.bindby) { lst.get_value = lst.dataset.bindby; } else if (lst.dataset.joinby) { - lst.get_value = get_value(lst.dataset.joinby); + lst.get_value = get_get_value(lst.dataset.joinby); } else { lst.get_value = get_get_value(); } @@ -103,8 +103,8 @@ function init_input_list() { const get_get_value = (join=',') => function () { return [...this.querySelectorAll('input')] .map(x => x.value) - .filter(x => x != '') - .join(join); + .filter(x => x != ''); + // .join(join); } /* -------------------------------------------------- */ diff --git a/static/jcal-tests.js b/static/jcal-tests.js new file mode 100644 index 00000000..c84d9bd1 --- /dev/null +++ b/static/jcal-tests.js @@ -0,0 +1,32 @@ +/* "Test cases" for jcal.js. + ideally we would actually have runnable tests, but + `document' is only available in the browser. +*/ + +let doc = document.implementation.createDocument(xcal, 'icalendar'); + +jcal = ['key', {}, 'text', 'value']; + +jcal_property_to_xcal_property(doc, jcal); + + + +jcal_to_xcal(['vcalendar', [], [['vevent', [['key', {}, 'text', 'value']], []]]]).childNodes[0].outerHTML + +/* returns (except not pretty printee) +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties/> + <components> + <vevent> + <properties> + <key> + <text>value</text> + </key> + </properties> + <components/> + </vevent> + </components> + </vcalendar> +</icalendar> +*/ diff --git a/static/jcal.js b/static/jcal.js new file mode 100644 index 00000000..da17a19e --- /dev/null +++ b/static/jcal.js @@ -0,0 +1,174 @@ +function jcal_type_to_xcal(doc, type, value) { + let el = doc.createElementNS(xcal, type); + switch (type) { + case 'boolean': + el.innerHTML = value ? "true" : "false"; + break; + + case 'float': + case 'integer': + el.innerHTML = '' + value; + break; + + case 'period': + let [start, end] = value; + let startEl = doc.createElementNS(xcal, 'start'); + startEl.innerHTML = start; + let endEL; + if (end.find('P')) { + endEl = doc.createElementNS(xcal, 'duration'); + } else { + endEl = doc.createElementNS(xcal, 'end'); + } + endEl.innerHTML = end; + el.appendChild(startEl); + el.appendChild(endEl); + break; + + case 'recur': + for (var key in value) { + if (! value.hasOwnProperty(key)) continue; + let e = doc.createElementNS(xcal, key); + e.innerHTML = value[key]; + el.appendChild(e); + } + break; + + case 'date': + case 'time': + case 'date-time': + + case 'duration': + + case 'binary': + case 'text': + case 'uri': + case 'cal-address': + case 'utc-offset': + el.innerHTML = value; + break; + + default: + /* TODO error */ + } + return el; +} + +function jcal_property_to_xcal_property(doc, jcal) { + let [propertyName, params, type, ...values] = jcal; + + let tag = doc.createElementNS(xcal, propertyName); + + /* setup parameters */ + let paramEl = doc.createElementNS(xcal, 'params'); + for (var key in params) { + /* Check if the key actually belongs to us. + At least (my) format also appears when iterating + over the parameters. Probably a case of builtins + vs user defined. + + This is also the reason we can't check if params + is empty beforehand, and instead check the + number of children of paramEl below. + */ + if (! params.hasOwnProperty(key)) continue; + + let el = doc.createElementNS(xcal, key); + + for (let v of asList(params[key])) { + let text = doc.createElementNS(xcal, 'text'); + text.innerHTML = '' + v; + el.appendChild(text); + } + + paramEl.appendChild(el); + } + + if (paramEl.childCount > 0) { + tag.appendChild(paramEl); + } + + /* setup value (and type) */ + // let typeEl = doc.createElementNS(xcal, type); + + switch (propertyName) { + case 'geo': + if (type == 'float') { + // assert values[0] == [x, y] + let [x, y] = values[0]; + let lat = doc.createElementNS(xcal, 'latitude') + let lon = doc.createElementNS(xcal, 'longitude') + lat.innerHTML = x; + lon.innerHTML = y; + tag.appendChild(lat); + tag.appendChild(lon); + } else { + /* TODO, error */ + } + break; + case 'request-status': + if (type == 'text') { + // assert values[0] instanceof Array + let [code, desc, ...data] = values[0]; + let codeEl = doc.createElementNS(xcal, 'code') + code.innerHTML = code; + tag.appendChild(codeEl); + + + let descEl = doc.createElementNS(xcal, 'description') + desc.innerHTML = desc; + tag.appendChild(descEl); + + if (data !== []) { + data = data[0]; + let dataEl = doc.createElementNS(xcal, 'data') + data.innerHTML = data; + tag.appendChild(dataEl); + } + } else { + /* TODO, error */ + } + break; + default: + for (let value of values) { + tag.appendChild(jcal_type_to_xcal(doc, type, value)) + } + } + + return tag; +} + + +function jcal_to_xcal(...jcals) { + let doc = document.implementation.createDocument(xcal, 'icalendar'); + for (let jcal of jcals) { + doc.documentElement.appendChild(jcal_to_xcal_inner(doc, jcal)); + } + return doc; +} + +function jcal_to_xcal_inner(doc, jcal) { + let [tagname, properties, components] = jcal; + + let xcal_tag = doc.createElementNS(xcal, tagname); + + /* I'm not sure if the properties and components tag should be left out + when empty. It should however NOT be an error to leave them in. + */ + + let xcal_properties = doc.createElementNS(xcal, 'properties'); + for (let property of properties) { + xcal_properties.appendChild(jcal_property_to_xcal_property(doc, property)); + } + + let xcal_children = doc.createElementNS(xcal, 'components'); + for (let child of components) { + xcal_children.appendChild(jcal_to_xcal_inner(doc, child)); + } + + xcal_tag.appendChild(xcal_properties); + xcal_tag.appendChild(xcal_children); + + return xcal_tag; + +} diff --git a/static/lib.js b/static/lib.js index ab279353..1d42100c 100644 --- a/static/lib.js +++ b/static/lib.js @@ -32,6 +32,9 @@ function zip(...args) { NOTE that only the raw `get' (and NOT the `getUTC') methods should be used on these objects, and that the reported timezone is quite often wrong. + + TODO The years between 0 and 100 (inclusive) gives dates in the twentieth + century, due to how javascript works (...). */ function parseDate(str) { @@ -117,7 +120,16 @@ function setVar(str, val) { } +function asList(thing) { + if (thing instanceof Array) { + return thing; + } else { + return [thing]; + } +} + +/* internal */ function datepad(thing, width=2) { return (thing + "").padStart(width, "0"); } @@ -147,7 +159,7 @@ function format_date(date, str) { } return outstr; } -Object.prototype.format = function () { return this; } /* any number of arguments */ +Object.prototype.format = function () { return "" + this; } /* any number of arguments */ Date.prototype.format = function (str) { return format_date (this, str); } /* diff --git a/static/recur.js b/static/recur.js deleted file mode 100644 index e69de29b..00000000 --- a/static/recur.js +++ /dev/null diff --git a/static/rrule.js b/static/rrule.js index abc648af..67a4453f 100644 --- a/static/rrule.js +++ b/static/rrule.js @@ -8,6 +8,14 @@ function recur_xml_to_rrule(dom_element) { return rr; } +function recur_jcal_to_rrule(jcal) { + let rr = new RRule; + for (var key in jcal) { + rr[key] = jcal[key]; + } + return rr; +} + class RRule { /* direct access to fields is fine */ @@ -17,7 +25,9 @@ class RRule { fields = ['freq', 'until', 'count', 'interval', 'bysecond', 'byminute', 'byhour', 'bymonthday', 'byyearday', 'byweekno', - 'bymonth', 'bysetpos', 'wkst'] + 'bymonth', 'bysetpos', 'wkst', + 'byday' + ] constructor() { @@ -49,16 +59,33 @@ class RRule { this.listeners[field].push(proc); } - asXcal() { + /* NOTE this function is probably never used. + Deperate it and refer to RRule.asJcal + together with jcal_to_xcal */ + asXcal(doc) { /* TODO empty case */ - let str = "<recur>"; + // let str = "<recur>"; + let root = doc.createElementNS(xcal, 'recur'); for (let f of this.fields) { let v = this.fields[f]; if (! v) continue; - str += `<${f}>${v}</${f}>`; + let tag = doc.createElementNS(xcal, f); + /* TODO type formatting */ + tag.innerHTML = `${v}`; + root.appendChild(tag); + } + return root; + } + + asJcal() { + let obj = {}; + for (let f of this.fields) { + let v = this[f]; + if (! v) continue; + /* TODO special formatting for some types */ + obj[f] = v; } - str += "</recur>"; - return str; + return obj; } /* diff --git a/static/script.js b/static/script.js index 312da431..6b7ddcd9 100644 --- a/static/script.js +++ b/static/script.js @@ -383,7 +383,9 @@ window.onload = function () { */ function get_property(el, field, default_value) { if (! el.properties) { + /* TODO only have construction once */ el.properties = {}; + el.properties.ical_properties = new Set() } if (! el.properties["_slot_" + field]) { @@ -424,6 +426,7 @@ function get_property(el, field, default_value) { function bind_properties (el, wide_event=false) { el.properties = {} + el.properties.ical_properties = new Set() let popup = popup_from_event(el); // let children = el.getElementsByTagName("properties")[0].children; @@ -524,6 +527,9 @@ function bind_properties (el, wide_event=false) { }]); } + for (let property of property_names) { + el.properties.ical_properties.add(property) + } /* icalendar properties */ for (let child of el.querySelector("vevent > properties").children) { @@ -532,29 +538,15 @@ function bind_properties (el, wide_event=false) { let field = child.tagName; let lst = get_property(el, field); + el.properties.ical_properties.add(field) /* Bind vcomponent fields for this event */ for (let s of el.querySelectorAll(`${field} > :not(parameters)`)) { - lst.push([s, (s, v) => { - if (v instanceof Date) { - if (v.isWholeDay) { - let str = v.format('~Y-~m-~d'); - child.innerHTML = `<date>${str}</date>`; - } else { - let str = v.format('~Y-~m-~dT~H:~M:00~Z'); - child.innerHTML = `<date-time>${str}</date-time>`; - } - } else if (v instanceof RRule) { - child.innerHTML = v.asXcal(); - } else { - /* assume that type already is correct */ - s.innerHTML = v; - } - }]); - /* Binds value from XML-tree to javascript object [parsedate] + + TODO capture xcal type here, to enable us to output it to jcal later. */ switch (field) { case 'rrule': @@ -566,29 +558,6 @@ function bind_properties (el, wide_event=false) { } } - /* Dynamicly add or remove the <location/> and <description/> elements - from the <vevent><properties/> list. - - TODO generalize this to all fields, /especially/ those which are - dynamicly added. - */ - for (let field of ['location', 'description', 'categories']) { - get_property(el, field).push( - [el.querySelector('vevent > properties'), - (s, v) => { - let slot = s.querySelector(field); - if (v === '' && slot) { - slot.remove(); - } else { - if (! slot) { - /* finns det verkligen inget bättre sätt... */ - s.innerHTML += `<${field}><text/></${field}>`; - } - s.querySelector(`${field} > text`).innerHTML = v; - } - }]); - } - /* set up graphical display changes */ let container = el.closest(".event-container"); if (container === null) { @@ -618,28 +587,6 @@ function bind_properties (el, wide_event=false) { } - /* Update XML on rrule field change */ - if (el.properties.rrule) { - for (let f of el.properties.rrule.fields) { - el.properties.rrule.addListener( - f, v => { - console.log(v); - let recur = el.querySelector('rrule recur'); - let field = recur.querySelector(f); - if (field) { - if (! v) { - field.remove(); - } else { - field.innerHTML = v; - } - } else { - if (v) recur.innerHTML += `<${f}>${v}</${f}>`; - } - }); - } - } - - /* ---------- Calendar ------------------------------ */ if (! el.dataset.calendar) { diff --git a/static/server_connect.js b/static/server_connect.js index e789d72c..9794d87e 100644 --- a/static/server_connect.js +++ b/static/server_connect.js @@ -21,16 +21,90 @@ async function remove_event (element) { } } +function event_to_jcal (event) { + let properties = []; + + for (let prop of event.properties.ical_properties) { + let v = event.properties[prop]; + if (v !== undefined) { + + let type = 'text'; + let value; + + if (v instanceof Array) { + } else if (v instanceof Date) { + if (v.isWholeDay) { + type = 'date'; + value = v.format("~Y-~m-~d"); + } else { + type = 'date-time'; + /* TODO TZ */ + value = v.format("~Y-~m-~dT~H:~M:~S"); + } + } else if (v === true || v === false) { + type = 'boolean'; + value = v; + } else if (typeof(v) == 'number') { + /* TODO float or integer */ + type = 'integer'; + value = v; + } else if (v instanceof RRule) { + type = 'recur'; + value = v.asJcal(); + } + /* TODO period */ + else { + /* text types */ + value = v; + } + + properties.push([prop, {}, type, value]); + } + } + + return ['vevent', properties, [/* alarms go here */]] +} + async function create_event (event) { - let xml = event.getElementsByTagName("icalendar")[0].outerHTML + // let xml = event.getElementsByTagName("icalendar")[0].outerHTML let calendar = event.properties.calendar; - console.log(calendar, xml); + console.log(calendar/*, xml*/); let data = new URLSearchParams(); data.append("cal", calendar); - data.append("data", xml); + // data.append("data", xml); + + console.log(event); + + + + let jcal = + ['vcalendar', + [ + /* + 'prodid' and 'version' are technically both required (RFC 5545, + 3.6 Calendar Components). + */ + ], + [ + /* vtimezone goes here */ + event_to_jcal(event), + ] + ]; + + console.log(jcal); + + let doc = jcal_to_xcal(jcal); + console.log(doc); + let str = doc.childNodes[0].outerHTML; + console.log(str); + data.append("data", str); + + // console.log(event.properties); + + // return; let response = await fetch ( '/insert', { method: 'POST', @@ -54,12 +128,12 @@ async function create_event (event) { */ let parser = new DOMParser(); - let properties = parser + let return_properties = parser .parseFromString(body, 'text/xml') .children[0]; let child; - while ((child = properties.firstChild)) { + while ((child = return_properties.firstChild)) { let target = event.querySelector( "vevent properties " + child.tagName); if (target) { diff --git a/static/style.scss b/static/style.scss index 4cd6b410..2d6b87b6 100644 --- a/static/style.scss +++ b/static/style.scss @@ -173,6 +173,10 @@ html, body { } } +li > button { + width: 100%; +} + /* Eventlist ---------------------------------------- The sidebar with all the events @@ -944,6 +948,10 @@ along with their colors. background-color: var(--color); } +.side-by-side { + display: flex; +} + /* Icalendar ---------------------------------------- */ diff --git a/static/types.js b/static/types.js index cfed8584..9a4aa01c 100644 --- a/static/types.js +++ b/static/types.js @@ -15,6 +15,17 @@ let all_types = [ 'boolean', ] +let property_names = [ + 'calscale', 'method', 'prodid', 'version', 'attach', 'categories', + 'class', 'comment', 'description', 'geo', 'location', 'percent-complete', + 'priority', 'resources', 'status', 'summary', 'completed', 'dtend', 'due', + 'dtstart', 'duration', 'freebusy', 'transp', 'tzid', 'tzname', 'tzoffsetfrom', + 'tzoffsetto', 'tzurl', 'attendee', 'contact', 'organizer', 'recurrence-id', + 'related-to', 'url', 'uid', 'exdate', 'exrule', 'rdate', 'rrule', 'action', + 'repeat', 'trigger', 'created', 'dtstamp', 'last-modified', 'sequence', 'request-status' +]; + + let valid_fields = { 'VCALENDAR': ['PRODID', 'VERSION', 'CALSCALE', 'METHOD'], 'VEVENT': ['DTSTAMP', 'UID', 'DTSTART', 'CLASS', 'CREATED', |