aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHugo Hörnquist <hugo@lysator.liu.se>2021-05-17 01:32:25 +0200
committerHugo Hörnquist <hugo@lysator.liu.se>2021-05-17 01:32:25 +0200
commit6c21cb7b669a6778e57f7043c15446a38a1fc614 (patch)
treed4df2d192ab5c6b6f8a4c4bd8e3e6b0b4b36d8b8
parentTerminal reload events when jumping to today. (diff)
parentLong events now wholeday per default. (diff)
downloadcalp-6c21cb7b669a6778e57f7043c15446a38a1fc614.tar.gz
calp-6c21cb7b669a6778e57f7043c15446a38a1fc614.tar.xz
Merge branch 'jcal'
-rw-r--r--README1
-rw-r--r--doc/ref/javascript/binders.texi10
-rw-r--r--doc/ref/javascript/date_time.texi2
-rw-r--r--doc/ref/javascript/lib.texi3
-rw-r--r--module/calp/html/vcomponent.scm81
-rw-r--r--module/calp/html/view/calendar.scm6
-rw-r--r--module/calp/repl.scm7
-rw-r--r--module/vcomponent/xcal/parse.scm26
-rw-r--r--static/binders.js52
-rw-r--r--static/date_time.js54
-rw-r--r--static/script.js280
-rw-r--r--static/server_connect.js75
-rw-r--r--static/style.scss28
-rw-r--r--static/types.js12
-rw-r--r--static/vcal.js362
-rw-r--r--tests/recurrence-simple.scm24
16 files changed, 616 insertions, 407 deletions
diff --git a/README b/README
index 3bf535db..ef1cb239 100644
--- a/README
+++ b/README
@@ -36,6 +36,7 @@ Standards and specifications
----------------------------
- RFC 5545 (iCalendar)
- RFC 6321 (xCal)
+- RFC 7265 (jCal)
- Vdir Storage Format [VDIR]
Building
diff --git a/doc/ref/javascript/binders.texi b/doc/ref/javascript/binders.texi
index 2b64b230..0e38411b 100644
--- a/doc/ref/javascript/binders.texi
+++ b/doc/ref/javascript/binders.texi
@@ -1,5 +1,7 @@
@node binders
+@cindex binder
+@cindex binders
@subsection binders.js
The bind system allows HTML-elements to specify that they want to be
@@ -45,3 +47,11 @@ Binder for the wholeday toggle button.
While CSS would suffice, this sets the disabled flags on the time
inputs, giving a better user experience.
@end defun
+
+@defun bind_date_time el e
+@anchor{bind_date_time}
+For @code{date_time} dummy component. Propagates gets and sets to
+underlying input fields.
+
+Note: @emph{Must} be called @emph{after} @code{init_date_time}.
+@end defun
diff --git a/doc/ref/javascript/date_time.texi b/doc/ref/javascript/date_time.texi
index fb2563f1..b2c5db92 100644
--- a/doc/ref/javascript/date_time.texi
+++ b/doc/ref/javascript/date_time.texi
@@ -36,4 +36,6 @@ components. We nevertheless use it here since we are emulating an
input element.
@end defivar
+See also @pxref{bind_date_time}
+
@end defun
diff --git a/doc/ref/javascript/lib.texi b/doc/ref/javascript/lib.texi
index ec5d4450..8adb8621 100644
--- a/doc/ref/javascript/lib.texi
+++ b/doc/ref/javascript/lib.texi
@@ -102,6 +102,9 @@ Boolean indicating if the time component of the Date object should be disregarde
@defun parseDate str
Takes a string @var{str}, which should be in ISO-8601 date-format, and
returns a javascript Date object.
+Handles date-times, with and without seconds, trailing `Z' for
+time-zones, and dates without times.
+If no time is given the @code{dateonly} attribute is set to yes.
@end defun
@defun copyDate date
diff --git a/module/calp/html/vcomponent.scm b/module/calp/html/vcomponent.scm
index 9764f513..ca38bdf7 100644
--- a/module/calp/html/vcomponent.scm
+++ b/module/calp/html/vcomponent.scm
@@ -160,44 +160,42 @@
(end (prop ev 'DTEND)))
`(div (@ (class "timeinput"))
- (input (@ (type "date")
- (name "dtstart-date")
- (style "grid-column:1;grid-row:2")
- (class "bind")
- (data-property "--dtstart-date")
- (value ,(date->string (as-date start)))))
-
- (input (@ (type "date")
- (name "dtend-date")
- (style "grid-column:1;grid-row:3")
- (class "bind")
- (data-property "--dtend-date")
- ,@(when end `((value ,(date->string (as-date end)))))))
-
,@(with-label
- "Heldag?"
- `(input (@ (type "checkbox")
- (class "bind")
- (data-bindby "bind_wholeday")
- (style "display:none")
- (name "wholeday"))))
-
- (input (@ (type "time")
- (name "dtstart-time")
- (class "bind")
- (data-property "--dtstart-time")
- (style "grid-column:3;grid-row:2;"
- ,(when (date? start) "display:none"))
- (value ,(time->string (as-time start)))))
-
- (input (@ (type "time")
- (name "dtend-time")
- (class "bind")
- (data-property "--dtend-time")
- (style "grid-column:3;grid-row:3;"
- ,(when (date? end) "display:none"))
- ,@(when end `((value ,(time->string (as-time end)))))
- ))))
+ "Starttid"
+ `(div (@ (class "date-time bind")
+ (data-bindby "bind_date_time")
+ (name "dtstart"))
+ (input (@ (type "date")
+ (value ,(date->string (as-date start)))))
+ (input (@ (type "time")
+ (value ,(time->string (as-time start) "~H:~M"))
+ ,@(when (date? start) '((disabled)))
+ ))))
+
+ ;; TODO some way to add an endtime if missing beforehand
+ ;; TODO, actually proper support for event without end times
+ ,@(when end
+ (with-label
+ "Sluttid"
+ `(div (@ (class "date-time bind")
+ (data-bindby "bind_date_time")
+ (name "dtend"))
+ (input (@ (type "date")
+ (value ,(date->string (as-date end)))))
+ (input (@ (type "time")
+ (value ,(time->string (as-time end) "~H:~M"))
+ ,@(when (date? end) '((disabled))))))))
+
+ (div
+ ,@(with-label
+ "Heldag?"
+ `(input (@ (type "checkbox")
+ (class "bind")
+ (data-bindby "bind_wholeday")
+ (name "wholeday")
+ ,@(when (date? start) '((checked)))))))
+
+ ))
,@(with-label
"Plats"
@@ -544,8 +542,9 @@
`(("📅" title: "Översikt"
,(fmt-single-event ev))
- ("📅" title: "Redigera"
- ,(fmt-for-edit ev))
+ ,@(when (edit-mode)
+ `(("📅" title: "Redigera"
+ ,(fmt-for-edit ev))))
("⤓" title: "Nedladdning"
(div (@ (class "eventtext") (style "font-family:sans"))
@@ -558,7 +557,9 @@
,@(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"))))))
+ (li (button (@ (onclick "console.log(jcal_to_xcal(event_to_jcal(event_from_popup(this.closest('.popup-container')))));")) "xml"))
+ (li (button (@ (onclick "console.log(event_from_popup(this.closest('.popup-container')))")) "this"))
+ ))))
))
,@(when (prop ev 'RRULE)
diff --git a/module/calp/html/view/calendar.scm b/module/calp/html/view/calendar.scm
index 3f607bb7..f84d2133 100644
--- a/module/calp/html/view/calendar.scm
+++ b/module/calp/html/view/calendar.scm
@@ -115,8 +115,12 @@
(script (@ (defer) (src "/static/server_connect.js")))
(script (@ (defer) (src "/static/input_list.js")))
(script (@ (defer) (src "/static/date_time.js")))
+ (script (@ (defer) (src "/static/vcal.js")))
(script (@ (defer) (src "/static/script.js")))
- ,(calendar-styles calendars))
+ ,(calendar-styles calendars)
+
+ ,@(when (debug)
+ '((style ".root { background-color: pink; }"))))
(body
(div (@ (class "root"))
diff --git a/module/calp/repl.scm b/module/calp/repl.scm
index d4f087aa..e6fbfe3d 100644
--- a/module/calp/repl.scm
+++ b/module/calp/repl.scm
@@ -31,4 +31,9 @@
[(address port) (make-tcp-server-socket host: address port: port)])
(string-split address #\:))]
;; currently impossible
- [(IPv6) (error "How did you get here?")])))
+ [(IPv6) (error "How did you get here?")]))
+
+ ;; TODO setup repl environment here
+
+
+ )
diff --git a/module/vcomponent/xcal/parse.scm b/module/vcomponent/xcal/parse.scm
index 6ae8c2f9..124a91f4 100644
--- a/module/vcomponent/xcal/parse.scm
+++ b/module/vcomponent/xcal/parse.scm
@@ -58,7 +58,31 @@
((@ (vcomponent recurrence parse)
rfc->datetime-weekday)
(string->symbol v)))
- (else v))))))]
+ ((freq) (string->symbol v))
+ ((until)
+ ;; RFC 6321 (xcal), p. 30 specifies type-until as
+ ;; type-until = element until {
+ ;; type-date |
+ ;; type-date-time
+ ;; }
+ ;; but doesn't bother defining type-date[-time]...
+ ;; This is acknowledged in errata 3315 [1], but
+ ;; it lacks a solution...
+ ;; Seeing as RFC 7265 (jcal) in Example 2 (p. 16)
+ ;; show the date as a direct string we will roll
+ ;; with that here to.
+ ;; [1]: https://www.rfc-editor.org/errata/eid3315
+ (string->date/-time v))
+ ((byday) #|TODO|#
+ (throw 'not-yet-implemented))
+ ((count interval bysecond bymunite byhour
+ bymonthday byyearday byweekno
+ bymonth bysetpos)
+ (string->number v))
+ (else (throw
+ 'key-error
+ "Invalid key ~a, with value ~a"
+ k v)))))))]
[(time) (parse-iso-time (car value))]
diff --git a/static/binders.js b/static/binders.js
index 197fb368..12d968e4 100644
--- a/static/binders.js
+++ b/static/binders.js
@@ -7,7 +7,7 @@
function bind_recur(el, e) {
/* todo bind default slots of rrule */
- let p = get_property(el, 'rrule');
+ let p = el.properties.get_callback_list('rrule');
// let rrule = el.rrule;
/* add listeners to bind-rr tags */
@@ -62,7 +62,7 @@ function bind_recur(el, e) {
}
function bind_edit(el, e) {
- let p = get_property(el, e.dataset.property);
+ let p = el.properties.get_callback_list(e.dataset.property);
e.addEventListener('input', function () {
el.properties[e.dataset.property] = this.value;
});
@@ -89,8 +89,8 @@ function bind_edit(el, e) {
}
function bind_view(el, e) {
- let f = (s, v) => s.innerHTML = v.format(s.dataset && s.dataset.fmt);
- get_property(el, e.dataset.property).push([e, f]);
+ let f = (s, v) => s.innerHTML = v.format(s.dataset && s.dataset.fmt);
+ el.properties.get_callback_list(e.dataset.property).push([e, f]);
}
@@ -103,10 +103,50 @@ function bind_wholeday(el, e) {
}
for (let f of ['dtstart', 'dtend']) {
- let d = el.properties[f];
- if (! d) continue; /* dtend optional */
+ let param = el.properties[f];
+ if (! param) continue; /* dtend optional */
+ let d = param.value;
+ if (wholeday.checked) {
+ param.type = 'date';
+ } else {
+ param.type = 'date-time';
+ }
d.isWholeDay = wholeday.checked;
el.properties[f] = d;
}
});
}
+
+
+/* used for dtstart and dtend input boxes
+ init_date_time MUST be called beforehand
+*/
+function bind_date_time(el, e) {
+ e.addEventListener('input', function () {
+ let dt = el.properties[e.name].value;
+ if (e.value == '') return;
+ let y, m, d, h, s;
+ switch (this.type) {
+ case 'date':
+ [y,m,d] = this.value.split('-')
+ dt.setYear(Number(y)/* - 1900 */);
+ dt.setMonth(Number(m) - 1);
+ dt.setDate(d);
+ break;
+ case 'time':
+ [h,m,s] = this.value.split(':')
+ dt.setHours(Number(h));
+ dt.setMinutes(Number(m));
+ dt.setSeconds(0);
+ break;
+ default:
+ console.log("How did you get here??? ", e);
+ }
+
+ el.properties[e.name] = dt;
+ });
+
+ el.properties.get_callback_list(e.name).push(
+ [e, (s, v) => s.value = v.format("~Y-~m-~dT~H:~M")]);
+
+}
diff --git a/static/date_time.js b/static/date_time.js
index 274d476f..8b7249dd 100644
--- a/static/date_time.js
+++ b/static/date_time.js
@@ -1,30 +1,36 @@
-function init_date_time() {
- for (let dt of document.getElementsByClassName("date-time")) {
- dt.time = dt.querySelector('[type=time]');
- dt.date = dt.querySelector('[type=date]');
+function init_date_time_single(dt) {
+ dt.time = dt.querySelector('[type=time]');
+ dt.date = dt.querySelector('[type=date]');
- Object.defineProperty(dt, 'value', {
- get: () => (dt.date.value && dt.time.value)
- // TODO wrapping <date-time/> tag
- ? dt.date.value + "T" + dt.time.value
- : "",
- set: (v) => [dt.date.value, dt.time.value] = v.split("T"),
- });
+ Object.defineProperty(dt, 'value', {
+ get: () => (dt.date.value && dt.time.value)
+ // TODO wrapping <date-time/> tag
+ ? dt.date.value + "T" + dt.time.value
+ : "",
+ set: (v) => [dt.date.value, dt.time.value] = v.split("T"),
+ });
- Object.defineProperty(dt, 'name', {
- get: () => dt.attributes.name.value
- });
+ Object.defineProperty(dt, 'name', {
+ get: () => dt.attributes.name.value
+ });
- dt._addEventListener = dt.addEventListener;
- dt.addEventListener = function (field, proc) {
- switch (field) {
- case 'input':
- dt.time.addEventListener(field, proc);
- dt.date.addEventListener(field, proc);
- break;
- default:
- dt._addEventListener(field, proc);
- }
+ dt._addEventListener = dt.addEventListener;
+ dt.addEventListener = function (field, proc) {
+ /* input events are propagated to children
+ other events target ourselves */
+ switch (field) {
+ case 'input':
+ dt.time.addEventListener(field, proc);
+ dt.date.addEventListener(field, proc);
+ break;
+ default:
+ dt._addEventListener(field, proc);
}
}
}
+
+function init_date_time() {
+ for (let dt of document.getElementsByClassName("date-time")) {
+ init_date_time_single(dt);
+ }
+}
diff --git a/static/script.js b/static/script.js
index 6b7ddcd9..7d3d2c29 100644
--- a/static/script.js
+++ b/static/script.js
@@ -14,16 +14,30 @@ class EventCreator {
}
create_empty_event () {
+ /* TODO this doesn't clone JS attributes */
+
let event = document.getElementById("event-template")
.firstChild.cloneNode(true);
let popup = document.getElementById("popup-template")
.firstChild.cloneNode(true);
+ /* -------------------- */
+ /* Manually transfer or recreate attributes which we still need */
+ /* TODO shouldn't these use transferListeners (or similar)?
+ See input_list.js:transferListeners */
+
+ for (let dt of popup.getElementsByClassName("date-time")) {
+ init_date_time_single(dt);
+ }
+
popup.getElementsByClassName("edit-form")[0].onsubmit = function () {
create_event(event);
return false; /* stop default */
}
+ /* -------------------- */
+ /* Fix tabs for newly created popup */
+
let id = gensym ("__js_event");
// TODO remove button?
@@ -41,8 +55,12 @@ class EventCreator {
let nav = popup.getElementsByClassName("popup-control")[0];
bind_popup_control(nav);
+ /* -------------------- */
+
// TODO download links
+ /* -------------------- */
+
event.id = id;
popup.id = "popup" + id;
@@ -113,7 +131,10 @@ class EventCreator {
this.appendChild(event);
/* requires that event is child of an '.event-container'. */
- bind_properties(event, wide_element);
+ new VComponent(
+ event,
+ wide_element=wide_element);
+ // bind_properties(event, wide_element);
/* requires that dtstart and dtend properties are initialized */
@@ -159,6 +180,8 @@ class EventCreator {
let d1 = new Date(container_start.getTime() + start_in_duration)
let d2 = new Date(container_start.getTime() + end_in_duration)
+ // console.log(that.event);
+ console.log(d1, d2);
that.event.properties.dtstart = d1;
that.event.properties.dtend = d2;
}
@@ -187,6 +210,8 @@ class EventCreator {
}
}
+
+
/* This incarnation of this function only adds the calendar switcher dropdown.
All events are already editable by switching to that tab.
@@ -227,6 +252,8 @@ function place_in_edit_mode (event) {
tab.querySelector("input[name='summary']").focus();
}
+
+
window.onload = function () {
// let start_time = document.querySelector("meta[name='start-time']").content;
// let end_time = document.querySelector("meta[name='end-time']").content;
@@ -250,6 +277,8 @@ window.onload = function () {
sch.update(d);
}, 1000 * 60);
+ init_date_time();
+
/* Is event creation active? */
if (EDIT_MODE) {
let eventCreator = new EventCreator;
@@ -283,7 +312,13 @@ window.onload = function () {
let popupElement = document.getElementById("popup" + event.id);
open_popup(popupElement);
- popupElement.querySelector("input[name='summary']").focus();
+ popupElement.querySelector("input[name='summary']").focus();
+
+ /* This assumes that it's unchecked beforehand.
+ Preferably we would just ensure that it's checked here,
+ But we also need to make sure that the proper handlers
+ are run then */
+ popupElement.querySelector("input[name='wholeday']").click();
});
}
@@ -300,7 +335,6 @@ window.onload = function () {
*/
el.parentElement.removeAttribute("href");
- /* TODO this doesn't yet apply to newly created events */
let popup = document.getElementById("popup" + el.id);
popup.getElementsByClassName("edit-form")[0].onsubmit = function () {
create_event(el);
@@ -309,9 +343,9 @@ window.onload = function () {
/* Bind all vcomponent properties into javascript. */
if (el.closest(".longevents")) {
- bind_properties(el, true);
+ new VComponent(el, true);
} else {
- bind_properties(el, false);
+ new VComponent(el, false);
}
}
@@ -368,243 +402,7 @@ window.onload = function () {
// init_arbitary_kv();
- init_date_time();
init_input_list();
}
-
-/*
- Returns the _value_ slot of given field in event, creating it if needed .
- el - the event to work on
- field - name of the field
- default_value - default value when creating
- bind_to_ical - should this property be added to the icalendar subtree?
-*/
-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]) {
- el.properties["_slot_" + field] = [];
- el.properties["_value_" + field] = default_value;
-
- Object.defineProperty(
- el.properties, field,
- {
- get: function () {
- return this["_value_" + field];
- },
- set: function (value) {
- this["_value_" + field] = value;
- for (let [slot,updater] of el.properties["_slot_" + field]) {
- updater(slot, value);
- }
- }
- });
- }
-
- return el.properties["_slot_" + field];
-}
-
-
-
-/*
- Properties are icalendar properties.
-
- p['name'] to get and set value (also updates any connected slots)
-
- p['_value_name'] for raw value
- p['_slot_name'] for connected slots, Vector of pairs, where the
- car should be a reference to the slot, and the
- cdr a procedure which takes a slot and a value
- and binds the value to the slot.
- */
-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;
-
- /* actual component (not popup) */
- /*
- for (let e of el.querySelectorAll(".bind")) {
- }
- */
-
- /* bind_recur */
-
- /* primary display tab */
-
- let p;
- let lst = [...popup.querySelectorAll(".bind"),
- ...el.querySelectorAll('.bind')];
- for (let e of lst) {
- if ((p = e.closest('[data-bindby]'))) {
- // console.log(p.dataset.bindby);
- eval(p.dataset.bindby)(el, e);
- } else {
- let f = ((s, v) => s.innerHTML = v.format(s.dataset && s.dataset.fmt));
- get_property(el, e.dataset.property).push([e, f]);
- }
- }
-
- // for (let e of popup.querySelectorAll(".summary-tab .bind")) {
- // /* bind_view
- // let f = (s, v) => s.innerHTML = v.format(s.dataset && s.dataset.fmt);
- // get_property(el, e.dataset.property).push([e, f]);
- // */
- // }
-
- /* edit tab */
- // for (let e of popup.querySelectorAll(".edit-tab .bind")) {
- // /* bind-edit
- // let p = get_property(el, e.dataset.property);
- // e.addEventListener('input', function () {
- // el.properties[e.dataset.property] = this.value;
- // });
- // let f;
- // switch (e.tagName) {
- // case 'input':
- // switch (e.type) {
- // case 'time': f = (s, v) => s.value = v.format("~H:~M"); break;
- // case 'date': f = (s, v) => s.value = v.format("~Y-~m-~d"); break;
- // // TODO remaining types cases
- // default: f = (s, v) => s.value = v;
- // }
- // p.push([e, f])
- // break;
- // case 'textarea':
- // f = (s, v) => s.innerHTML = v;
- // p.push([e, f])
- // break;
- // default:
- // alert("How did you get here??? " + e.tagName)
- // break;
- // }
- // */
- // }
-
- /* checkbox for whole day */
-
- for (let field of ['dtstart', 'dtend']) {
-
- get_property(el, `--${field}-time`).push(
- [el, (el, v) => { let date = el.properties[field];
- if (v == '') return;
- let [h,m,s] = v.split(':')
- date.setHours(Number(h));
- date.setMinutes(Number(m));
- date.setSeconds(0);
- el.properties[field] = date; }])
- get_property(el, `--${field}-date`).push(
- [el, (el, v) => { let date = el.properties[field];
- if (v == '') return;
- let [y,m,d] = v.split('-')
- date.setYear(Number(y)/* - 1900*/);
- date.setMonth(Number(m) - 1);
- date.setDate(d);
- el.properties[field] = date; }])
-
-
- /* Manual fetch of the fields instead of the general method,
- to avoid an infinite loop of dtstart setting --dtstart-time,
- and vice versa.
- NOTE if many more fields require special treatment then a
- general solution is required.
- */
- get_property(el, field).push(
- [el, (el, v) => { popup
- .querySelector(`.edit-tab input[name='${field}-time']`)
- .value = v.format("~H:~M");
- popup
- .querySelector(`.edit-tab input[name='${field}-date']`)
- .value = v.format("~Y-~m-~d");
- }]);
- }
-
- for (let property of property_names) {
- el.properties.ical_properties.add(property)
- }
-
- /* icalendar properties */
- for (let child of el.querySelector("vevent > properties").children) {
- /* child ≡ <dtstart><date-time>...</date-time></dtstart> */
-
- 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)`)) {
-
- /* 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':
- el.properties['_value_rrule'] = recur_xml_to_rrule(s);
- break;
- default:
- el.properties["_value_" + field] = s.innerHTML;
- }
- }
- }
-
- /* set up graphical display changes */
- let container = el.closest(".event-container");
- if (container === null) {
- console.log("No enclosing event container for", el);
- return;
- }
- let start = parseDate(container.dataset.start);
- let end = parseDate(container.dataset.end);
-
- if (el.properties.dtstart) {
- /* [parsedate] */
- el.properties.dtstart = parseDate(el.properties.dtstart);
- get_property(el, 'dtstart').push(
- [el.style, (s, v) =>
- s[wide_event?'left':'top'] = 100 * (to_local(v) - start)/(end - start) + "%"]);
- }
-
-
- if (el.properties.dtend) {
- el.properties.dtend = parseDate(el.properties.dtend);
- get_property(el, 'dtend').push(
- // TODO right and bottom only works if used from the start. However,
- // events from the backend instead use top/left and width/height.
- // Normalize so all use the same, or find a way to convert between.
- [el.style,
- (s, v) => s[wide_event?'right':'bottom'] = 100 * (1 - (to_local(v)-start)/(end-start)) + "%"]);
- }
-
-
- /* ---------- Calendar ------------------------------ */
-
- if (! el.dataset.calendar) {
- el.dataset.calendar = "Unknown";
- }
-
- let calprop = get_property(el, 'calendar', el.dataset.calendar);
-
- const rplcs = (s, v) => {
- let [_, calclass] = s.classList.find(/^CAL_/);
- s.classList.replace(calclass, "CAL_" + v);
- }
-
- calprop.push([popup, rplcs]);
- calprop.push([el, rplcs]);
- calprop.push([el, (s, v) => s.dataset.calendar = v]);
-
-
-
- /* ---------- Calendar ------------------------------ */
-}
diff --git a/static/server_connect.js b/static/server_connect.js
index a50128ae..f3fbac1b 100644
--- a/static/server_connect.js
+++ b/static/server_connect.js
@@ -22,55 +22,28 @@ 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 */]]
+ /* encapsulate event in a shim calendar, to ensure that
+ we always send correct stuff */
+ return ['vcalendar',
+ [
+ /*
+ 'prodid' and 'version' are technically both required (RFC 5545,
+ 3.6 Calendar Components).
+ */
+ ],
+ [
+ /* vtimezone goes here */
+ event.properties.to_jcal()
+ ]
+ ];
}
async function create_event (event) {
// let xml = event.getElementsByTagName("icalendar")[0].outerHTML
- let calendar = event.properties.calendar;
+ let calendar = event.properties.calendar.value;
- console.log(calendar/*, xml*/);
+ console.log('calendar=', calendar/*, xml*/);
let data = new URLSearchParams();
data.append("cal", calendar);
@@ -78,23 +51,9 @@ async function create_event (event) {
console.log(event);
+ let jcal = event_to_jcal(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);
diff --git a/static/style.scss b/static/style.scss
index 7080189d..87e62637 100644
--- a/static/style.scss
+++ b/static/style.scss
@@ -898,34 +898,6 @@ along with their colors.
}
.timeinput {
-
- display: grid;
- grid-template-columns: 1fr [lbl-start] 1ch 1fr 1ch [lbl-end];
- grid-template-rows: [lbl-start] 0.7fr 1fr 1fr 0.3fr [lbl-end];
-
- label {
- background-color: rgba(10,20,30,0.7);
- color: white;
- border-radius: 1em;
-
- grid-column: lbl-start / lbl-end;
- grid-row: lbl-start / lbl-end;
-
- text-align: center;
-
- user-select: none;
-
- z-index: 1;
-
- }
-
- input {
- z-index: 2;
- }
-
- input:checked ~ input {
- z-index: 0;
- }
}
}
diff --git a/static/types.js b/static/types.js
index 9a4aa01c..02ae2261 100644
--- a/static/types.js
+++ b/static/types.js
@@ -3,16 +3,16 @@ let all_types = [
'text',
'uri',
'binary',
- 'float',
- 'integer',
- 'date-time',
- 'date',
+ 'float', /* Number.type = 'float' */
+ 'integer', /* Number.type = 'integer' */
+ 'date-time', /* Date */
+ 'date', /* Date.dateonly = true */
'duration',
'period',
'utc-offset',
'cal-address',
- 'recur',
- 'boolean',
+ 'recur', /* RRule */
+ 'boolean', /* boolean */
]
let property_names = [
diff --git a/static/vcal.js b/static/vcal.js
new file mode 100644
index 00000000..079b09f8
--- /dev/null
+++ b/static/vcal.js
@@ -0,0 +1,362 @@
+/*
+ Properties are icalendar properties.
+
+ p['name'] to get and set value (also updates any connected slots)
+
+ p['_value_name'] for raw value
+ p['_slot_name'] for connected slots, Vector of pairs, where the
+ car should be a reference to the slot, and the
+ cdr a procedure which takes a slot and a value
+ and binds the value to the slot.
+ */
+class VComponent {
+
+ constructor(el, wide_event=false) {
+ el.properties = this;
+ this.html_element = el;
+
+ /*
+ List of field listeners, which are all notified whenever
+ the listened for field is updated.
+ - keys are field names
+ - values MUST be a pair of
+ + a javascript object to update
+ + a prodecude taking that object, and the new value
+ */
+ this._slots = {}
+
+ /* VCalParameter objects */
+ this._values = {}
+
+ /*
+ All properties on this object which are part of the vcomponent.
+ Ideally simply looping through all javascript fields would be nice,
+ but we only want to export some.
+
+ Popuplated by iCalendars built in types per default, and extended
+ */
+ this.ical_properties = new Set();
+
+ let popup = popup_from_event(el);
+ // let children = el.getElementsByTagName("properties")[0].children;
+
+ /* actual component (not popup) */
+ /*
+ for (let e of el.querySelectorAll(".bind")) {
+ }
+ */
+
+ /* bind_recur */
+
+ /* primary display tab */
+
+ let p;
+ let lst = [...popup.querySelectorAll(".bind"),
+ ...el.querySelectorAll('.bind')];
+ for (let e of lst) {
+ // if (e.classList.contains('summary')) {
+ // console.log(e, e.closest('[data-bindby]'));
+ // }
+ if ((p = e.closest('[data-bindby]'))) {
+ // console.log(p);
+ // console.log(p.dataset.bindby);
+ eval(p.dataset.bindby)(el, e);
+ } else {
+ // if (e.classList.contains('summary')) {
+ // /* TODO transfer data from backend to frontend in a better manner */
+ // console.log (this.get_callback_list(e.dataset.property));
+ // }
+ let f = (s, v) => {
+ console.log(s, v);
+ s.innerHTML = v.format(s.dataset && s.dataset.fmt);
+ };
+ this.get_callback_list(e.dataset.property).push([e, f]);
+ // if (e.classList.contains('summary')) {
+ // console.log (this.get_callback_list(e.dataset.property));
+ // }
+ // console.log("registreing", e, e.dataset.property, this);
+ }
+ }
+
+ /* checkbox for whole day */
+
+ /* Popuplate default types, see types.js for property_names */
+ for (let property of property_names) {
+ this.ical_properties.add(property)
+ // console.log("prop", property)
+
+ this.create_property(property);
+ }
+
+ /* icalendar properties */
+ for (let child of el.querySelector("vevent > properties").children) {
+ /* child ≡ <dtstart><date-time>...</date-time></dtstart> */
+
+ let field = child.tagName;
+ // // let lst = get_property(el, field);
+ // let lst = this.get(field);
+
+ this.ical_properties.add(field)
+
+ /* Bind vcomponent fields for this event */
+ for (let s of el.querySelectorAll(`${field} > :not(parameters)`)) {
+ /* s ≡ <date-time>...</date-time> */
+
+ /* Binds value from XML-tree to javascript object
+ [parsedate]
+
+ TODO capture xcal type here, to enable us to output it to jcal later.
+ */
+ let parsedValue;
+ let type = s.tagName.toLowerCase();
+ switch (type) {
+ case 'float':
+ case 'integer':
+ parsedValue = Number(s.innerHTML);
+ break;
+
+ case 'date-time':
+ case 'date':
+ parsedValue = parseDate(s.innerHTML);
+ break;
+
+ /* TODO */
+ case 'duration':
+ let start = s.getElementsByTagName('start');
+ let end = s.getElementsByTagName('end, duration');
+ if (end.tagName === 'period') {
+ parsePeriod(end.innerHTML);
+ }
+ break;
+ /* TODO */
+ case 'period':
+ parsedValue = parsePeriod(s.innerHTML);
+ break;
+ /* TODO */
+ case 'utc-offset':
+ break;
+
+ case 'recur':
+ parsedValue = recur_xml_to_rrule(s);
+ break;
+
+ case 'boolean':
+ switch (s.innerHTML) {
+ case 'true': parsedValue = true; break;
+ case 'false': parsedValue = false; break;
+ default: throw "Value error"
+ }
+ break;
+
+
+ case 'binary':
+ /* Binary is going to be BASE64 decoded, allowing us to ignore
+ it and handle it as a string for the time being */
+ case 'cal-address':
+ case 'text':
+ case 'uri':
+ parsedValue = s.innerHTML;
+ // parsedValue.type = type;
+ break;
+
+ default:
+ parsedValue = s.innerHTML;
+ }
+
+
+ // this['_value_rrule'] = new VCalParameter(type, parsedValue);
+ // console.log("set", field, type, parsedValue);
+ this._values[field] = new VCalParameter(type, parsedValue);
+ if (! this._slots[field]) {
+ this._slots[field] = [];
+ }
+ }
+ }
+
+ /* set up graphical display changes */
+ let container = el.closest(".event-container");
+ if (container === null) {
+ console.log("No enclosing event container for", el);
+ return;
+ }
+ let start = parseDate(container.dataset.start);
+ let end = parseDate(container.dataset.end);
+
+ if (this.dtstart) {
+ /* [parsedate] */
+ // el.properties.dtstart = parseDate(el.properties.dtstart);
+ this.get_callback_list('dtstart').push(
+ [el.style, (s, v) => {
+ console.log(v);
+ s[wide_event?'left':'top'] = 100 * (to_local(v) - start)/(end - start) + "%";
+ } ]);
+ }
+
+
+ if (this.dtend) {
+ // el.properties.dtend = parseDate(el.properties.dtend);
+ this.get_callback_list('dtend').push(
+ // TODO right and bottom only works if used from the start. However,
+ // events from the backend instead use top/left and width/height.
+ // Normalize so all use the same, or find a way to convert between.
+ [el.style,
+ (s, v) => s[wide_event?'right':'bottom'] = 100 * (1 - (to_local(v)-start)/(end-start)) + "%"]);
+ }
+
+
+ /* ---------- Calendar ------------------------------ */
+
+ if (! el.dataset.calendar) {
+ el.dataset.calendar = "Unknown";
+ }
+
+ let calprop = this.get_callback_list('calendar');
+ this.create_property('calendar');
+ this._values['calendar'] =
+ new VCalParameter('INVALID', el.dataset.calendar);
+
+ const rplcs = (s, v) => {
+ let [_, calclass] = s.classList.find(/^CAL_/);
+ s.classList.replace(calclass, "CAL_" + v);
+ }
+
+ calprop.push([popup, rplcs]);
+ calprop.push([el, rplcs]);
+ calprop.push([el, (s, v) => s.dataset.calendar = v]);
+
+
+
+ /* ---------- /Calendar ------------------------------ */
+ }
+
+
+ /*
+ Returns the _value_ slot of given field in event, creating it if needed .
+ el - the event to work on
+ field - name of the field
+ default_value - default value when creating
+ bind_to_ical - should this property be added to the icalendar subtree?
+ */
+ get_callback_list(field) {
+ // let el = this.html_element;
+ if (! this._slots[field]) {
+ this._slots[field] = [];
+ }
+
+ // console.log("get", field);
+ return this._slots[field];
+ }
+
+ to_jcal() {
+ let properties = [];
+
+ /* ??? */
+ // for (let prop of event.properties.ical_properties) {
+ for (let prop of this.ical_properties) {
+ // console.log(prop);
+ let v = this[prop];
+ if (v !== undefined) {
+ let sub = v.to_jcal();
+ sub.unshift(prop);
+ properties.push(sub);
+ }
+ }
+
+ return ['vevent', properties, [/* alarms go here */]]
+ }
+
+ create_property(property_name) {
+ Object.defineProperty(
+ this, property_name,
+ {
+ /* TODO there is an assymetry here with .value needing to be called for
+ get:ed stuff, but set MUST be an unwrapped item.
+ Fix this.
+ */
+ get: function() {
+ return this._values[property_name];
+ },
+ set: function (value) {
+ console.log("set", property_name, value);
+ this._values[property_name].value = value;
+ console.log(this._slots[property_name].length,
+ this._slots[property_name]);
+ /* TODO validate type */
+ /* See valid_input_types and all_types */
+ for (let [slot,updater] of this._slots[property_name]) {
+ // console.log(updater, slot);
+ updater(slot, value);
+ }
+ },
+ });
+ }
+
+}
+
+
+
+/* "Body" of a vcomponent field.
+ For example, given the JCal
+ ["dtstamp", {}, "date-time", "2006-02-06T00:11:21Z"],
+ this class would have
+ VCalParameter {
+ type = "date-time",
+ properties = {},
+ _value = new Date(2006,1,6,0,11,21)
+ }
+ And returns [{}, "date-time", "2006-02-06T00:11:21Z"]
+ when serialized
+ */
+class VCalParameter {
+ constructor (type, value, properties={}) {
+ this.type = type;
+ this._value = value;
+ this.properties = properties;
+ }
+
+ get value() {
+ return this._value;
+ }
+
+ set value(v) {
+ this._value = v;
+ }
+
+ to_jcal() {
+ let value;
+ let v = this._value;
+ switch (this.type) {
+ case 'binary':
+ /* TOOD */
+ break;
+ case 'date-time':
+ value = v.format("~Y-~m-~dT~H:~M:~S");
+ // TODO TZ
+ break;
+ case 'date':
+ value = v.format("~Y-~m-~d");
+ break;
+ case 'duration':
+ /* TODO */
+ break;
+ case 'period':
+ /* TODO */
+ break;
+ case 'utc-offset':
+ /* TODO */
+ break;
+ case 'recur':
+ value = v.asJcal();
+ break;
+
+ case 'float':
+ case 'integer':
+ case 'text':
+ case 'uri':
+ case 'cal-address':
+ case 'boolean':
+ value = v;
+ }
+ return [this.properties, this.type, value];
+ }
+}
diff --git a/tests/recurrence-simple.scm b/tests/recurrence-simple.scm
index 9c78977b..166fa349 100644
--- a/tests/recurrence-simple.scm
+++ b/tests/recurrence-simple.scm
@@ -12,7 +12,8 @@
((guile) format)
((vcomponent) parse-calendar)
- ((vcomponent recurrence)
+ ((vcomponent xcal parse) sxcal->vcomponent)
+ ((vcomponent recurrence)
parse-recurrence-rule
make-recur-rule
generate-recurrence-set))
@@ -238,3 +239,24 @@ END:VCALENDAR"
(test-assert "Changing type on Recurrence id."
(stream->list 10 (generate-recurrence-set ev)))
+
+;;; Earlier I failed to actually parse the recurrence parts, in short, 1 ≠ "1".
+
+(define ev
+ (sxcal->vcomponent
+ '(vevent
+ (properties
+ (summary (text "reptest"))
+ (dtend (date-time "2021-01-13T02:00:00"))
+ (dtstart (date-time "2021-01-13T01:00:00"))
+ (uid (text "RNW198S6QANQPV1C4FDNFH6ER1VZX6KXEYNB"))
+ (rrule (recur (freq "WEEKLY")
+ (interval "1")
+ (wkst "MO")))
+ (dtstamp (date-time "2021-01-13T01:42:20Z"))
+ (sequence (integer "0")))
+ (components))))
+
+(test-assert
+ "Check that recurrence rule commint from xcal also works"
+ (generate-recurrence-set ev))