From 8ec2f441d40ab89b40cc3158f65c914eff497cee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Mon, 4 Oct 2021 23:18:24 +0200 Subject: Major typescript work. --- module/calp/html/view/calendar.scm | 2 +- module/calp/html/view/calendar/week.scm | 4 +- static/.gitignore | 10 ++ static/clock.ts | 53 ++++--- static/dragable.ts | 23 +-- static/globals.ts | 211 +++++++++++++++---------- static/jcal.ts | 143 +++++++++-------- static/lib.ts | 154 +++++++++++------- static/popup.ts | 76 +++++---- static/rrule.ts | 100 ------------ static/rrule.ts.disabled | 100 ++++++++++++ static/script.ts | 193 ++++++++++++----------- static/server_connect.ts | 89 ++++++----- static/types.ts | 228 +++++++++++++++++---------- static/vevent.ts | 268 +++++++++++++++++--------------- 15 files changed, 947 insertions(+), 707 deletions(-) delete mode 100644 static/rrule.ts create mode 100644 static/rrule.ts.disabled diff --git a/module/calp/html/view/calendar.scm b/module/calp/html/view/calendar.scm index dfcd2264..c328f8b3 100644 --- a/module/calp/html/view/calendar.scm +++ b/module/calp/html/view/calendar.scm @@ -110,7 +110,7 @@ (script (@ (defer) (src "/static/dragable.js"))) (script (@ (defer) (src "/static/clock.js"))) (script (@ (defer) (src "/static/popup.js"))) - (script (@ (defer) (src "/static/rrule.js"))) + ;; (script (@ (defer) (src "/static/rrule.js"))) ;; (script (@ (defer) (src "/static/binders.js"))) (script (@ (defer) (src "/static/server_connect.js"))) ;; (script (@ (defer) (src "/static/input_list.js"))) diff --git a/module/calp/html/view/calendar/week.scm b/module/calp/html/view/calendar/week.scm index 9911b162..17bb3b2d 100644 --- a/module/calp/html/view/calendar/week.scm +++ b/module/calp/html/view/calendar/week.scm @@ -15,7 +15,7 @@ :use-module ((calp html vcomponent) :select (make-block) ) :use-module ((calp html components) - :select (btn tabset #; #; form with-label + :select (btn tabset ; form with-label )) :use-module ((vcomponent group) :select (group-stream get-groups-between)) @@ -170,7 +170,7 @@ ;; (eq? 'TRANSPARENT (prop ev 'TRANSP))) ;; " transparent") ) - (onclick "toggle_popup('popup' + this.id)") + ; (onclick "toggle_popup('popup' + this.id)") ) ;; Inner div to prevent overflow. Previously "overflow: none" ;; was set on the surounding div, but the popup /needs/ to diff --git a/static/.gitignore b/static/.gitignore index 735b5dce..3153016b 100644 --- a/static/.gitignore +++ b/static/.gitignore @@ -1,3 +1,13 @@ *.css .*-cache *.map +clock.js +dragable.js +globals.js +jcal.js +lib.js +popup.js +script.js +server_connect.js +types.js +vevent.js diff --git a/static/clock.ts b/static/clock.ts index d33d603a..c4feda8f 100644 --- a/static/clock.ts +++ b/static/clock.ts @@ -1,31 +1,35 @@ class Clock { - update(now) { + update(now: Date) { } } class Timebar extends Clock { - constructor(start_time, end_time) { + // start_time: Date + // end_time: Date + bar_object: HTMLElement | null + + constructor(/*start_time: Date, end_time: Date*/) { super(); - this.start_time = start_time; - this.end_time = end_time; - this.bar_object = false + // this.start_time = start_time; + // this.end_time = end_time; + this.bar_object = null } - update(now) { + update(now: Date) { // if (! (this.start_time <= now.getTime() && now.getTime() < this.end_time)) // return; var event_area = document.getElementById(now.format("~Y-~m-~d")) if (event_area) { - if (this.bar_object) { + if (this.bar_object !== null && this.bar_object.parentNode !== null) { this.bar_object.parentNode.removeChild(this.bar_object) } else { - this.bar_object = makeElement ('div', { + this.bar_object = makeElement('div', { id: 'bar', className: 'eventlike current-time', }); @@ -38,13 +42,17 @@ class Timebar extends Clock { } class SmallcalCellHighlight extends Clock { - constructor(small_cal) { + + small_cal: HTMLElement + current_cell: HTMLElement | null + + constructor(small_cal: HTMLElement) { super(); this.small_cal = small_cal; - this.current_cell = false + this.current_cell = null } - update(now) { + update(now: Date) { if (this.current_cell) { this.current_cell.style.border = ""; } @@ -63,35 +71,42 @@ class SmallcalCellHighlight extends Clock { /* -------------------------------------------------- */ class ClockElement extends HTMLElement { - constructor () { + + timer_id: number + + constructor() { super(); + + this.timer_id = 0 } - connectedCallback () { - let interval = this.hasAttribute('interval') ? +this.getAttribute('img') : 60; + connectedCallback() { + let interval = this.hasAttribute('interval') + ? +(this.getAttribute('interval') as string) + : 60; interval *= 1000 /* ms */ this.timer_id = window.setInterval(() => this.update(new Date), interval) this.update(new Date) } - static get observedAttributes () { + static get observedAttributes() { return ['timer_id'] } - update (now) { /* noop */ } + update(now: Date) { /* noop */ } } class TodayButton extends ClockElement { - update (now) { - this.querySelector('a').href = now.format("~Y-~m-~d.html") + update(now: Date) { + (this.querySelector('a') as any).href = now.format("~Y-~m-~d.html") } } customElements.define('today-button', TodayButton) class CurrentTime extends ClockElement { - update (now) { + update(now: Date) { this.innerHTML = now.format('~H:~M:~S') } } diff --git a/static/dragable.ts b/static/dragable.ts index 6eb0b999..20acdd3a 100644 --- a/static/dragable.ts +++ b/static/dragable.ts @@ -9,34 +9,35 @@ /* Given the navbar of a popup, make it dragable. */ -function bind_popup_control (nav) { +function bind_popup_control(nav: HTMLElement) { - if (! nav.closest('.popup-container')) { - throw TypeError('not a popup container'); - } + // if (!nav.closest('popup-element')) { + // console.log(nav); + // throw TypeError('not a popup container'); + // } - nav.onmousedown = function (e) { + nav.onmousedown = function(e) { /* Ignore mousedown on children */ if (e.target != nav) return; nav.style.cursor = "grabbing"; nav.dataset.grabbed = "true"; nav.dataset.grabPoint = e.clientX + ";" + e.clientY; // let popup = nav.closest(".popup-container"); - let popup = nav.closest("popup-element"); + let popup = nav.closest("popup-element") as HTMLElement; nav.dataset.startPoint = popup.offsetLeft + ";" + popup.offsetTop; } - window.addEventListener('mousemove', function (e) { + window.addEventListener('mousemove', function(e) { if (nav.dataset.grabbed) { - let [x, y] = nav.dataset.grabPoint.split(";").map(Number); - let [startX, startY] = nav.dataset.startPoint.split(";").map(Number); + let [x, y] = nav.dataset.grabPoint!.split(";").map(Number); + let [startX, startY] = nav.dataset.startPoint!.split(";").map(Number); // let popup = nav.closest(".popup-container"); - let popup = nav.closest("popup-element"); + let popup = nav.closest("popup-element") as HTMLElement; popup.style.left = startX + (e.clientX - x) + "px"; popup.style.top = startY + (e.clientY - y) + "px"; } }); - window.addEventListener('mouseup', function () { + window.addEventListener('mouseup', function() { nav.dataset.grabbed = ""; nav.style.cursor = ""; }); diff --git a/static/globals.ts b/static/globals.ts index 86368e9a..64a3613f 100644 --- a/static/globals.ts +++ b/static/globals.ts @@ -1,15 +1,18 @@ "use strict"; -const vcal_objects = {}; +const vcal_objects: Map = new Map() class ComponentVEvent extends HTMLElement { - constructor () { - super (); - this.template = document.getElementById(this.tagName); + + template: HTMLTemplateElement + + constructor() { + super(); + this.template = document.getElementById(this.tagName) as HTMLTemplateElement; let uid; if ((uid = this.dataset.uid)) { - vcal_objects[uid].register(this); + vcal_objects.get(uid)?.register(this); } /* We DON'T have a redraw here in the general case, since the @@ -18,24 +21,26 @@ class ComponentVEvent extends HTMLElement { should take care of that some other way */ } - connectedCallback () { - let uid; + connectedCallback() { + let uid, v; if ((uid = this.dataset.uid)) { - this.redraw (vcal_objects[uid]); + v = vcal_objects.get(uid) + if (v) this.redraw(v); } } - redraw (data) { + redraw(data: VEvent) { // update ourselves from template - if (! this.template) { + if (!this.template) { throw "Something"; } - let body = this.template.content.cloneNode(true).firstElementChild; + let body = (this.template.content.cloneNode(true) as DocumentFragment).firstElementChild!; for (let el of body.getElementsByClassName("bind")) { - let p = el.dataset.property; + if (!(el instanceof HTMLElement)) continue; + let p = el.dataset.property!; let d, fmt; if ((d = data.getProperty(p))) { if ((fmt = el.dataset.fmt)) { @@ -52,17 +57,27 @@ class ComponentVEvent extends HTMLElement { } class ComponentDescription extends ComponentVEvent { - constructor () { - super() ; + constructor() { + super(); } } class ComponentEdit extends ComponentVEvent { - constructor () { + + firstTime: boolean + uid: string + + constructor() { super(); this.firstTime = true; + + if (this.dataset.uid === undefined) { + throw "data-uid must be set" + } else { + this.uid = this.dataset.uid; + } } connectedCallback() { @@ -70,50 +85,59 @@ class ComponentEdit extends ComponentVEvent { /* Edit tab is rendered here. It's left blank server-side, since it only makes sense to have something here if we have javascript */ - let data = vcal_objects[this.dataset.uid] + let data = vcal_objects.get(this.uid) - if (! data) { + if (!data) { throw `Data missing for uid ${this.dataset.uid}.` } this.redraw(data); + return; + for (let el of this.getElementsByClassName("interactive")) { el.addEventListener('input', () => { - vcal_objects[this.dataset.uid].setProperty( - el.dataset.property, + let obj = vcal_objects.get(this.uid) + if (obj === undefined) { + throw 'No object with uid ' + this.uid + } + if (!(el instanceof HTMLInputElement)) return; + obj.setProperty( + el.dataset.property!, el.value) }); } } - redraw (data) { + redraw(data: VEvent) { // update ourselves from template - if (! this.template) { + if (!this.template) { throw "Something"; } let body; if (this.firstTime) { - body = this.template.content.cloneNode(true).firstElementChild; + body = (this.template.content.cloneNode(true) as DocumentFragment).firstElementChild!; } else { body = this; } for (let el of body.getElementsByClassName("interactive")) { - let p = el.dataset.property; - let d; + if (!(el instanceof HTMLInputElement)) continue; + let p = el.dataset.property!; + let d: any; if ((d = data.getProperty(p))) { /* https://stackoverflow.com/questions/57157830/how-can-i-specify-the-sequence-of-running-nested-web-components-constructors */ - window.setTimeout (() => { + window.setTimeout(() => { /* NOTE Some specific types might require special formatting here. But due to my custom components implementing custom `.value' procedures, we might not need any special cases here */ - el.value = d; + console.log(el, d); + (el as HTMLInputElement).value = d; }); } } @@ -126,59 +150,68 @@ class ComponentEdit extends ComponentVEvent { } -function find_popup (uid) { +function find_popup(uid: uid): HTMLElement | null { // for (let el of vcal_objects[uid].registered) { // if (el.tagName === 'popup-element') { // return el; // } // } // throw 'Popup not fonud'; - return document.querySelector(`popup-element[data-uid="${uid}"]`) + return document.querySelector(`popup-element[data-uid="${uid}"]`) as HTMLElement } -function find_block (uid) { - for (let el of vcal_objects[uid].registered) { +function find_block(uid: uid): HTMLElement | null { + let obj = vcal_objects.get(uid) + if (obj === undefined) { + return null; + } + for (let el of obj.registered) { if (el.tagName === 'vevent-block') { return el; } } - throw 'Popup not fonud'; + // throw 'Popup not fonud'; + return null; } class ComponentBlock extends ComponentVEvent { - constructor () { + constructor() { super(); this.addEventListener('click', () => { - toggle_popup(find_popup(this.dataset.uid)); + let uid = this.dataset.uid + if (uid === undefined) throw new Error('UID missing from' + this) + let popup = find_popup(uid); + if (popup === null) throw new Error('no popup for uid ' + uid); + toggle_popup(popup); }); } - redraw (data) { + redraw(data: VEvent) { super.redraw(data); let p; if ((p = data.getProperty('dtstart'))) { - this.style.top = date_to_percent(to_local(p), 1) + "%"; + this.style.top = date_to_percent(to_local(p)) + "%"; // console.log('dtstart', p); } if ((p = data.getProperty('dtend'))) { this.style.height = 'unset'; // console.log('dtend', p); - this.style.bottom = (100 - date_to_percent(to_local(p), 1)) + "%"; + this.style.bottom = (100 - date_to_percent(to_local(p))) + "%"; } } } -window.addEventListener('load', function () { +window.addEventListener('load', function() { // let json_objects_el = document.getElementById('json-objects'); - let div = document.getElementById('xcal-data'); - let vevents = div.firstElementChild.childNodes; + let div = document.getElementById('xcal-data')!; + let vevents = div.firstElementChild!.children; for (let vevent of vevents) { let ev = xml_to_vcal(vevent); - vcal_objects[ev.getProperty('uid')] = ev + vcal_objects.set(ev.getProperty('uid'), ev) } /* @@ -206,49 +239,56 @@ window.addEventListener('load', function () { -class DateTimeInput extends HTMLElement { - constructor () { + +class DateTimeInput extends /* HTMLInputElement */ HTMLElement { + constructor() { super(); this.innerHTML = '' + console.log('constructing datetime input') } - static get observedAttributes () { - return [ 'dateonly' ] + static get observedAttributes() { + return ['dateonly'] } - attributeChangedCallback (name, from, to) { + attributeChangedCallback(name: string, from: any, to: any) { console.log(this, name, boolean(from), boolean(to)); switch (name) { - case 'dateonly': - this.querySelector('[type="time"]').disabled = boolean(to) - break; + case 'dateonly': + (this.querySelector('input[type="time"]') as HTMLInputElement) + .disabled = boolean(to) + break; } } - get dateonly () { + get dateonly(): boolean { return boolean(this.getAttribute('dateonly')); } - set dateonly (bool) { - this.setAttribute ('dateonly', bool); + set dateonly(bool: boolean) { + this.setAttribute('dateonly', "" + bool); } - get value () { - + get valueAsDate(): Date { let dt; - let date = this.querySelector("[type='date']").value; + let date = (this.querySelector("input[type='date']") as HTMLInputElement).value; if (boolean(this.getAttribute('dateonly'))) { dt = parseDate(date); dt.type = 'date'; } else { - let time = this.querySelector("[type='time']").value; + let time = (this.querySelector("input[type='time']") as HTMLInputElement).value; dt = parseDate(date + 'T' + time) dt.type = 'date-time'; } return dt; } - set value (new_value) { + get value(): string { + return this.valueAsDate.format("~Y-~m-~dT~H:~M:~S") + } + + set value(new_value: Date | string) { + console.log('Setting date'); let date, time; if (new_value instanceof Date) { date = new_value.format("~L~Y-~m-~d"); @@ -256,36 +296,42 @@ class DateTimeInput extends HTMLElement { } else { [date, time] = new_value.split('T') } - this.querySelector("[type='date']").value = date; - this.querySelector("[type='time']").value = time; + (this.querySelector("input[type='date']") as HTMLInputElement).value = date; + (this.querySelector("input[type='time']") as HTMLInputElement).value = time; } - addEventListener(type, proc) { + addEventListener(type: string, proc: ((e: Event) => void)) { if (type != 'input') throw "Only input supported"; - this.querySelector("[type='date']").addEventListener(type, proc); - this.querySelector("[type='time']").addEventListener(type, proc); + (this.querySelector("input[type='date']") as HTMLInputElement) + .addEventListener(type, proc); + (this.querySelector("input[type='time']") as HTMLInputElement) + .addEventListener(type, proc); } } -customElements.define('date-time-input', DateTimeInput) +customElements.define('date-time-input', DateTimeInput /*, { extends: 'input' } */) class PopupElement extends HTMLElement { - constructor () { + constructor() { super(); /* TODO populate remaining */ // this.id = 'popup' + this.dataset.uid } - redraw () { + redraw() { console.log('IMPLEMENT ME'); } connectedCallback() { - let body = document.getElementById('popup-template').content.cloneNode(true).firstElementChild; + let template: HTMLTemplateElement = document.getElementById('popup-template') as HTMLTemplateElement + let body = (template.content.cloneNode(true) as DocumentFragment).firstElementChild!; - let uid = this.dataset.uid + if (this.dataset.uid === null) { + throw 'UID is required' + } + let uid = this.dataset.uid! // console.log(uid); body.getElementsByClassName('populate-with-uid') @@ -295,43 +341,42 @@ class PopupElement extends HTMLElement { let tabgroup_id = gensym(); for (let tab of body.querySelectorAll(".tabgroup .tab")) { let new_id = gensym(); - let input = tab.querySelector("input"); + let input = tab.querySelector("input")!; input.id = new_id; input.name = tabgroup_id; - tab.querySelector("label").setAttribute('for', new_id); + tab.querySelector("label")!.setAttribute('for', new_id); } /* end tabs */ /* nav bar */ - let nav = body.getElementsByClassName("popup-control")[0]; + let nav = body.getElementsByClassName("popup-control")[0] as HTMLElement; bind_popup_control(nav); - let btn = body.querySelector('.popup-control .close-tooltip') - btn.addEventListener('click', () => { - close_popup(this); - }); + let btn = body.querySelector('.popup-control .close-tooltip') as HTMLButtonElement + btn.addEventListener('click', () => close_popup(this)); /* end nav bar */ this.replaceChildren(body); let that = this; - this.getElementsByClassName("calendar-selection") - .addEventListener('change', function () { - let uid = that.closest('[data-uid]').dataset.uid - let obj = vcal_objects[uid] - this.value; + this.getElementsByClassName("calendar-selection")[0] + .addEventListener('change', function() { + let uid = (that.closest('[data-uid]') as HTMLElement).dataset.uid! + let obj = vcal_objects.get(uid) + // TODO this procedure + // this.value; // event.properties.calendar = this.value; }); } } -window.addEventListener('load', function () { +window.addEventListener('load', function() { customElements.define('popup-element', PopupElement) }); -function wholeday_checkbox (box) { - box.closest('.timeinput') - .getElementsByTagName('date-time-input') - .forEach(el => el.dateonly = box.checked); +function wholeday_checkbox(box: HTMLInputElement) { + box.closest('.timeinput')! + .querySelectorAll('input[is="date-time"]') + .forEach((el) => { (el as DateTimeInput).dateonly = box.checked }); } diff --git a/static/jcal.ts b/static/jcal.ts index 003294d1..db833a3c 100644 --- a/static/jcal.ts +++ b/static/jcal.ts @@ -1,60 +1,63 @@ -function jcal_type_to_xcal(doc, type, value) { +function jcal_type_to_xcal(doc: Document, type: ical_type, value: any): Element { let el = doc.createElementNS(xcal, type); switch (type) { - case 'boolean': - el.textContent = value ? "true" : "false"; - break; - - case 'float': - case 'integer': - el.textContent = '' + value; - break; - - case 'period': - let [start, end] = value; - let startEl = doc.createElementNS(xcal, 'start'); - startEl.textContent = start; - let endEL; - if (end.find('P')) { - endEl = doc.createElementNS(xcal, 'duration'); - } else { - endEl = doc.createElementNS(xcal, 'end'); - } - endEl.textContent = 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.textContent = value[key]; - el.appendChild(e); - } - break; + case 'boolean': + el.textContent = value ? "true" : "false"; + break; + + case 'float': + case 'integer': + el.textContent = '' + value; + break; + + case 'period': + let [start, end] = value; + let startEl = doc.createElementNS(xcal, 'start'); + startEl.textContent = start; + let endEl: Element; + if (end.find('P')) { + endEl = doc.createElementNS(xcal, 'duration'); + } else { + endEl = doc.createElementNS(xcal, 'end'); + } + endEl.textContent = 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.textContent = value[key]; + el.appendChild(e); + } + break; - case 'date': - case 'time': - case 'date-time': + case 'date': + // case 'time': + case 'date-time': - case 'duration': + case 'duration': - case 'binary': - case 'text': - case 'uri': - case 'cal-address': - case 'utc-offset': - el.textContent = value; - break; + case 'binary': + case 'text': + case 'uri': + case 'cal-address': + case 'utc-offset': + el.textContent = value; + break; - default: + default: /* TODO error */ } return el; } -function jcal_property_to_xcal_property(doc, jcal) { +function jcal_property_to_xcal_property( + doc: Document, + jcal: JCalProperty +): Element { let [propertyName, params, type, ...values] = jcal; let tag = doc.createElementNS(xcal, propertyName); @@ -71,7 +74,7 @@ function jcal_property_to_xcal_property(doc, jcal) { is empty beforehand, and instead check the number of children of paramEl below. */ - if (! params.hasOwnProperty(key)) continue; + if (!params.hasOwnProperty(key)) continue; let el = doc.createElementNS(xcal, key); @@ -84,7 +87,7 @@ function jcal_property_to_xcal_property(doc, jcal) { paramEl.appendChild(el); } - if (paramEl.childCount > 0) { + if (paramEl.childElementCount > 0) { tag.appendChild(paramEl); } @@ -92,20 +95,21 @@ function jcal_property_to_xcal_property(doc, jcal) { // 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.textContent = x; - lon.textContent = y; - tag.appendChild(lat); - tag.appendChild(lon); - } else { - /* TODO, error */ - } - break; + 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.textContent = x; + lon.textContent = y; + tag.appendChild(lat); + tag.appendChild(lon); + } else { + /* TODO, error */ + } + break; + /* TODO reenable this case 'request-status': if (type == 'text') { // assert values[0] instanceof Array @@ -126,20 +130,21 @@ function jcal_property_to_xcal_property(doc, jcal) { tag.appendChild(dataEl); } } else { - /* TODO, error */ + /* TODO, error * / } break; - default: - for (let value of values) { - tag.appendChild(jcal_type_to_xcal(doc, type, value)) - } + */ + default: + for (let value of values) { + tag.appendChild(jcal_type_to_xcal(doc, type, value)) + } } return tag; } -function jcal_to_xcal(...jcals) { +function jcal_to_xcal(...jcals: JCal[]): Document { let doc = document.implementation.createDocument(xcal, 'icalendar'); for (let jcal of jcals) { doc.documentElement.appendChild(jcal_to_xcal_inner(doc, jcal)); @@ -147,7 +152,7 @@ function jcal_to_xcal(...jcals) { return doc; } -function jcal_to_xcal_inner(doc, jcal) { +function jcal_to_xcal_inner(doc: Document, jcal: JCal) { let [tagname, properties, components] = jcal; let xcal_tag = doc.createElementNS(xcal, tagname); diff --git a/static/lib.ts b/static/lib.ts index 100f4161..35b6f867 100644 --- a/static/lib.ts +++ b/static/lib.ts @@ -3,10 +3,39 @@ General procedures which in theory could be used anywhere. */ +interface Object { + format: (fmt: string) => string +} + +interface HTMLElement { + _addEventListener: (name: string, proc: (e: Event) => void) => void + listeners: Record void)[]> +} + +interface Date { + dateonly: boolean + utc: boolean + type: 'date' | 'date-time' +} + +interface DOMTokenList { + find: (regex: string) => [number, string] | undefined +} + +interface HTMLCollection { + forEach: (proc: ((el: Element) => void)) => void +} + +interface HTMLCollectionOf { + forEach: (proc: ((el: T) => void)) => void +} + +type uid = string + HTMLElement.prototype._addEventListener = HTMLElement.prototype.addEventListener; -HTMLElement.prototype.addEventListener = function (name, proc) { - if (! this.listeners) this.listeners = {}; - if (! this.listeners[name]) this.listeners[name] = []; +HTMLElement.prototype.addEventListener = function(name: string, proc: (e: Event) => void) { + if (!this.listeners) this.listeners = {}; + if (!this.listeners[name]) this.listeners[name] = []; this.listeners[name].push(proc); return this._addEventListener(name, proc); }; @@ -14,7 +43,8 @@ HTMLElement.prototype.addEventListener = function (name, proc) { /* list of lists -> list of tuples */ -function zip(...args) { +/* TODO figure out how to type this correctly */ +function zip(...args: any[]) { // console.log(args); if (args === []) return []; return [...Array(Math.min(...args.map(x => x.length))).keys()] @@ -37,8 +67,14 @@ function zip(...args) { century, due to how javascript works (...). */ -function parseDate(str) { - let year, month, day, hour=false, minute, second=0, utc; +function parseDate(str: string): Date { + let year: number; + let month: number; + let day: number; + let hour: number | false = false; + let minute: number = 0; + let second: number = 0; + let utc: boolean = false; let end = str.length - 1; if (str[end] == 'Z') { @@ -47,18 +83,18 @@ function parseDate(str) { }; switch (str.length) { - case '2020-01-01T13:37:00'.length: - second = str.substr(17,2); - case '2020-01-01T13:37'.length: - hour = str.substr(11,2); - minute = str.substr(14,2); - case '2020-01-01'.length: - year = str.substr(0,4); - month = str.substr(5,2) - 1; - day = str.substr(8,2); - break; - default: - throw 'Bad argument'; + case '2020-01-01T13:37:00'.length: + second = +str.substr(17, 2); + case '2020-01-01T13:37'.length: + hour = +str.substr(11, 2); + minute = +str.substr(14, 2); + case '2020-01-01'.length: + year = +str.substr(0, 4); + month = +str.substr(5, 2) - 1; + day = +str.substr(8, 2); + break; + default: + throw 'Bad argument'; } let date; @@ -73,32 +109,32 @@ function parseDate(str) { return date; } -function copyDate(date) { +function copyDate(date: Date): Date { let d = new Date(date); d.utc = date.utc; d.dateonly = date.dateonly; return d; } -function to_local(date) { - if (! date.utc) return date; +function to_local(date: Date): Date { + if (!date.utc) return date; return new Date(date.getTime() - date.getTimezoneOffset() * 60 * 1000); } /* -------------------------------------------------- */ -function makeElement (name, attr={}) { - let element = document.createElement(name); +function makeElement(name: string, attr = {}): HTMLElement { + let element: HTMLElement = document.createElement(name); for (let [key, value] of Object.entries(attr)) { - element[key] = value; + (element as any)[key] = value; } return element; } -function round_time (time, fraction) { +function round_time(time: number, fraction: number): number { let scale = 1 / fraction; - return Math.round (time * scale) / scale; + return Math.round(time * scale) / scale; } /* only used by the bar. @@ -107,20 +143,20 @@ function round_time (time, fraction) { Just doing (new Date()/(86400*1000)) would be nice, but there's no good way to get the time in the current day. */ -function date_to_percent (date) { - return (date.getHours() + (date.getMinutes() / 60)) * 100/24; +function date_to_percent(date: Date): number /* in 0, 100 */ { + return (date.getHours() + (date.getMinutes() / 60)) * 100 / 24; } /* if only there was such a thing as a let to wrap around my lambda... */ /* js infix to not collide with stuff generated backend */ -const gensym = (counter => (prefix="gensym") => prefix + "js" + ++counter)(0) +const gensym = (counter => (prefix = "gensym") => prefix + "js" + ++counter)(0) -function setVar(str, val) { - document.documentElement.style.setProperty("--" + str, val); +function setVar(str: string, val: any) { + document.documentElement.style.setProperty("--" + str, val); } -function asList(thing) { +function asList(thing: Array | T): Array { if (thing instanceof Array) { return thing; } else { @@ -129,44 +165,44 @@ function asList(thing) { } -function boolean (value) { +function boolean(value: any): boolean { switch (typeof value) { - case 'string': - switch (value) { - case 'true': return true; - case 'false': return false; - case '': return false; - default: return true; - } - case 'boolean': - return value; - default: - return !! value; + case 'string': + switch (value) { + case 'true': return true; + case 'false': return false; + case '': return false; + default: return true; + } + case 'boolean': + return value; + default: + return !!value; } } /* internal */ -function datepad(thing, width=2) { +function datepad(thing: number | string, width = 2): string { return (thing + "").padStart(width, "0"); } -function format_date(date, str) { +function format_date(date: Date, str: string): string { let fmtmode = false; let outstr = ""; for (var i = 0; i < str.length; i++) { if (fmtmode) { switch (str[i]) { /* Moves the date into local time. */ - case 'L': date = to_local(date); break; - case 'Y': outstr += datepad(date.getFullYear(), 4); break; - case 'm': outstr += datepad(date.getMonth() + 1); break; - case 'd': outstr += datepad(date.getDate()); break; - case 'H': outstr += datepad(date.getHours()); break; - case 'M': outstr += datepad(date.getMinutes()); break; - case 'S': outstr += datepad(date.getSeconds()); break; - case 'Z': if (date.utc) outstr += 'Z'; break; + case 'L': date = to_local(date); break; + case 'Y': outstr += datepad(date.getFullYear(), 4); break; + case 'm': outstr += datepad(date.getMonth() + 1); break; + case 'd': outstr += datepad(date.getDate()); break; + case 'H': outstr += datepad(date.getHours()); break; + case 'M': outstr += datepad(date.getMinutes()); break; + case 'S': outstr += datepad(date.getSeconds()); break; + case 'Z': if (date.utc) outstr += 'Z'; break; } fmtmode = false; } else if (str[i] == '~') { @@ -178,17 +214,17 @@ function format_date(date, str) { return outstr; } -Object.prototype.format = function (/* any number of arguments */) { return "" + this; } -Date.prototype.format = function (str) { return format_date (this, str); } +Object.prototype.format = function(/* any number of arguments */) { return "" + this; } +Date.prototype.format = function(str) { return format_date(this, str); } /* * Finds the first element of the DOMTokenList whichs value matches * the supplied regexp. Returns a pair of the index and the value. */ -DOMTokenList.prototype.find = function (regexp) { +DOMTokenList.prototype.find = function(regexp) { let entries = this.entries(); let entry; - while (! (entry = entries.next()).done) { + while (!(entry = entries.next()).done) { if (entry.value[1].match(regexp)) { return entry.value; } @@ -196,7 +232,7 @@ DOMTokenList.prototype.find = function (regexp) { } /* HTMLCollection is the result of a querySelectorAll */ -HTMLCollection.prototype.forEach = function (proc) { +HTMLCollection.prototype.forEach = function(proc) { for (let el of this) { proc(el); } diff --git a/static/popup.ts b/static/popup.ts index 0b04b280..8d9420c6 100644 --- a/static/popup.ts +++ b/static/popup.ts @@ -1,59 +1,70 @@ /* event component => coresponding popup component */ -function event_from_popup(popup) { +function event_from_popup(popup: Element): HTMLElement | null { // return document.getElementById(popup.id.substr(5)) - return find_block(popup.closest('[data-uid]').dataset.uid) + let el = popup.closest('[data-uid]') + if (!el) return null; + let uid = (el as HTMLElement).dataset.uid + if (!uid) return null; + return find_block(uid) } /* popup component => coresponding event component */ -function popup_from_event(event) { +function popup_from_event(event: Element): HTMLElement | null { // return document.getElementById("popup" + event.id); - return find_popup(event.closest('[data-uid]').dataset.uid) + // return find_popup(event.closest('[data-uid]').dataset.uid) + let el = event.closest('[data-uid]') + if (!el) return null; + let uid = (el as HTMLElement).dataset.uid + if (!uid) return null; + return find_popup(uid) } /* hides given popup */ -function close_popup(popup) { +function close_popup(popup: Element): void { popup.classList.remove("visible"); } /* hides all popups */ -function close_all_popups () { +function close_all_popups() { for (let popup of document.querySelectorAll("popup-element.visible")) { close_popup(popup); } } +declare let VIEW: 'month' | 'week' + /* open given popup */ -function open_popup(popup) { +function open_popup(popup: HTMLElement) { popup.classList.add("visible"); let element = event_from_popup(popup); // let root = document.body; let root; switch (VIEW) { - case 'week': - root = document.getElementsByClassName("days")[0]; - break; - case 'month': - default: - root = document.body; - break; + case 'week': + root = document.getElementsByClassName("days")[0]; + break; + case 'month': + default: + root = document.body; + break; } /* start sets offset between top left corner of event in calendar and popup. 10, 10 soo old event is still visible */ let offsetX = 10, offsetY = 10; - while (element !== root) { + while (element !== root && element !== null) { offsetX += element.offsetLeft; offsetY += element.offsetTop; - element = element.offsetParent; + element = element.offsetParent as HTMLElement; } popup.style.left = offsetX + "px"; popup.style.top = offsetY + "px"; } /* toggles open/closed status of popup given by id */ -function toggle_popup(popup) { +function toggle_popup(popup: HTMLElement) { // let popup = document.getElementById(popup_id); if (popup.classList.contains("visible")) { close_popup(popup); @@ -66,39 +77,38 @@ function toggle_popup(popup) { /* Makes the popup last hovered over the selected popup, moving it to * the top, and allowing global keyboard bindings to affect it. */ -let activePopup; +let activePopup: PopupElement | undefined; -for (let popup of document.querySelectorAll('.popup-container')) { +for (let popup of document.querySelectorAll('popup-element')) { /* TODO possibly only change "active" element after a fraction of * a second, for example when moving between tabs */ - popup.addEventListener('mouseover', function () { + popup.addEventListener('mouseover', function() { /* This is ever so slightly inefficient, but it really dosen't mammet */ for (let other of - document.querySelectorAll('.popup-container')) - { + document.querySelectorAll('popup-element')) { /* TODO get this from somewhere */ /* Currently it's manually copied from the stylesheet */ - other.style['z-index'] = 1000; + ((other as PopupElement).style as any)['z-index'] = 1000; } - popup.style['z-index'] += 1; - activePopup = popup; + ((popup as PopupElement).style as any)['z-index'] += 1; + activePopup = popup as PopupElement }); } -document.addEventListener('keydown', function (event) { +document.addEventListener('keydown', function(event) { /* Physical key position, names are what that key would be in QWERTY */ let i = ({ - 'KeyQ': 0, - 'KeyW': 1, - 'KeyE': 2, - 'KeyR': 3, + 'KeyQ': 0, + 'KeyW': 1, + 'KeyE': 2, + 'KeyR': 3, })[event.code]; if (i === undefined) return - if (! activePopup) return; - let element = activePopup.querySelectorAll(".tab > label")[i]; - if (! element) return; + if (!activePopup) return; + let element: HTMLLabelElement | undefined = activePopup.querySelectorAll(".tab > label")[i] as HTMLLabelElement; + if (!element) return; element.click(); }); diff --git a/static/rrule.ts b/static/rrule.ts deleted file mode 100644 index e7377370..00000000 --- a/static/rrule.ts +++ /dev/null @@ -1,100 +0,0 @@ -function recur_xml_to_rrule(dom_element) { - let rr = new RRule; - for (let child of dom_element.children) { - let key = child.tagName; /* freq */ - let val = child.textContent; /* weekly */ - rr[key] = val; - } - 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 */ - /* setting them however requires methods, since there might - be listeners */ - - fields = ['freq', 'until', 'count', 'interval', - 'bysecond', 'byminute', 'byhour', - 'bymonthday', 'byyearday', 'byweekno', - 'bymonth', 'bysetpos', 'wkst', - 'byday' - ] - - constructor() { - - this.listeners = {} - - for (let f of this.fields) { - this[f] = false; - Object.defineProperty( - this, f, { - /* - TODO many of the fields should be wrapped - in type tags. e.g. elements are either - or , NOT a raw date. - by* fields should be wrapped with multiple values. - */ - get: () => this['_' + f], - set: (v) => { - this['_' + f] = v - for (let l of this.listeners[f]) { - l(v); - } - } - }); - this.listeners[f] = []; - } - } - - addListener(field, proc) { - this.listeners[field].push(proc); - } - - /* 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 = ""; - let root = doc.createElementNS(xcal, 'recur'); - for (let f of this.fields) { - let v = this.fields[f]; - if (! v) continue; - let tag = doc.createElementNS(xcal, f); - /* TODO type formatting */ - tag.textContent = `${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; - } - return obj; - } - - /* - asIcal() { - return this.fields - .map(f => [f, this.fields[f]]) - .filter([_, v] => v) - .map(([k, v]) => `${k}=${v}`) - .join(';'); - } - */ -}; diff --git a/static/rrule.ts.disabled b/static/rrule.ts.disabled new file mode 100644 index 00000000..f210ee77 --- /dev/null +++ b/static/rrule.ts.disabled @@ -0,0 +1,100 @@ +function recur_xml_to_rrule(dom_element) { + let rr = new RRule; + for (let child of dom_element.children) { + let key = child.tagName; /* freq */ + let val = child.textContent; /* weekly */ + rr[key] = val; + } + 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 */ + /* setting them however requires methods, since there might + be listeners */ + + fields = ['freq', 'until', 'count', 'interval', + 'bysecond', 'byminute', 'byhour', + 'bymonthday', 'byyearday', 'byweekno', + 'bymonth', 'bysetpos', 'wkst', + 'byday' + ] + + constructor() { + + this.listeners = {} + + for (let f of this.fields) { + this[f] = false; + Object.defineProperty( + this, f, { + /* + TODO many of the fields should be wrapped + in type tags. e.g. elements are either + or , NOT a raw date. + by* fields should be wrapped with multiple values. + */ + get: () => this['_' + f], + set: (v) => { + this['_' + f] = v + for (let l of this.listeners[f]) { + l(v); + } + } + }); + this.listeners[f] = []; + } + } + + addListener(field, proc) { + this.listeners[field].push(proc); + } + + /* 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 = ""; + let root = doc.createElementNS(xcal, 'recur'); + for (let f of this.fields) { + let v = this.fields[f]; + if (!v) continue; + let tag = doc.createElementNS(xcal, f); + /* TODO type formatting */ + tag.textContent = `${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; + } + return obj; + } + + /* + asIcal() { + return this.fields + .map(f => [f, this.fields[f]]) + .filter([_, v] => v) + .map(([k, v]) => `${k}=${v}`) + .join(';'); + } + */ +}; diff --git a/static/script.ts b/static/script.ts index 16ff7bbd..8984c19a 100644 --- a/static/script.ts +++ b/static/script.ts @@ -6,6 +6,10 @@ class EventCreator { + event: HTMLElement | false + event_start: { x: number, y: number } + down_on_event: boolean + /* dynamicly created event when dragging */ constructor() { this.event = false; @@ -13,7 +17,7 @@ class EventCreator { this.down_on_event = false; } - create_empty_event () { + create_empty_event() { /* TODO this doesn't clone JS attributes */ // let event = document.getElementById("event-template") @@ -40,7 +44,7 @@ class EventCreator { /* -------------------- */ /* Fix tabs for newly created popup */ - let id = gensym ("__js_event"); + let id = gensym("__js_event"); // TODO remove button? // $("button 2??").onclick = `remove_event(document.getElementById('${id}'))` @@ -65,15 +69,15 @@ class EventCreator { /* -------------------- */ - event.id = id; - popup.id = "popup" + id; + // event.id = id; + // popup.id = "popup" + id; - return [popup, event]; + // return [popup, event]; } - create_event_down (intended_target) { + create_event_down(intended_target: HTMLElement): (e: MouseEvent) => any { let that = this; - return function (e) { + return function(e: MouseEvent) { /* Only trigger event creation stuff on actuall events background, NOT on its children */ that.down_on_event = false; @@ -93,20 +97,23 @@ class EventCreator { (event → [0, 1)), 𝐑, bool → event → () */ - create_event_move(pos_in, round_to=1, wide_element=false) { + create_event_move( + pos_in: ((c: HTMLElement, e: MouseEvent) => number), + round_to: number = 1, + wide_element: boolean = false + ): ((e: MouseEvent) => any) { let that = this; - return function (e) { - if (e.buttons != 1 || ! that.down_on_event) return; + return function(this: HTMLElement, e: MouseEvent) { + if (e.buttons != 1 || !that.down_on_event) return; /* Create event when we start moving the mouse. */ - if (! that.event) { + if (!that.event) { /* Small deadzone so tiny click and drags aren't registered */ if (Math.abs(that.event_start.x - e.clientX) < 10 - && Math.abs(that.event_start.y - e.clientY) < 5) - { return; } + && Math.abs(that.event_start.y - e.clientY) < 5) { return; } /* only allow start of dragging on background */ - if (e.target != this) return; + if (e.target !== this) return; /* only on left click */ if (e.buttons != 1) return; @@ -128,8 +135,8 @@ class EventCreator { */ let time = round_time(pos_in(this, e), round_to); - that.event.dataset.time1 = time; - that.event.dataset.time2 = time; + that.event.dataset.time1 = '' + time; + that.event.dataset.time2 = '' + time; /* ---------------------------------------- */ @@ -151,31 +158,32 @@ class EventCreator { This includes ourselves. */ for (let e of this.children) { - e.style.pointerEvents = "none"; + (e as HTMLElement).style.pointerEvents = "none"; } } let time1 = Number(that.event.dataset.time1); - let time2 = that.event.dataset.time2 = - round_time(pos_in(that.event.parentElement, e), - round_to); + let time2 = round_time( + pos_in(that.event.parentElement!, e), + round_to); + that.event.dataset.time2 = '' + time2 /* ---------------------------------------- */ - let event_container = that.event.closest(".event-container"); + let event_container = that.event.closest(".event-container") as HTMLElement; /* These two are in UTC */ - let container_start = parseDate(event_container.dataset.start); - let container_end = parseDate(event_container.dataset.end); + let container_start = parseDate(event_container.dataset.start!); + let container_end = parseDate(event_container.dataset.end!); /* ---------------------------------------- */ /* ms */ - let duration = container_end - container_start; + let duration = container_end.valueOf() - container_start.valueOf(); - let start_in_duration = duration * Math.min(time1,time2); - let end_in_duration = duration * Math.max(time1,time2); + let start_in_duration = duration * Math.min(time1, time2); + let end_in_duration = duration * Math.max(time1, time2); /* Notice that these are converted to UTC, since the intervals are given in utc, and I only really care about local time (which a specific local @@ -187,37 +195,40 @@ class EventCreator { // console.log(that.event); console.log(d1.format("~L~H:~M"), d2.format("~L~H:~M")); - that.event.redraw({ getProperty: (name) => - ({ dtstart: d1, dtend: d2 })[name]}); + // TODO + // (that.event as Redrawable).redraw({ + // getProperty: (name) => + // ({ dtstart: d1, dtend: d2 })[name] + // }); // that.event.properties.dtstart = d1; // that.event.properties.dtend = d2; } } - create_event_finisher (callback) { + create_event_finisher(callback: ((event: VEvent) => void)) { let that = this; - return function create_event_up (e) { - if (! that.event) return; + return function create_event_up(e: MouseEvent) { + if (!that.event) return; /* Restore pointer events for all existing events. Allow pointer events on our new event */ - for (let e of that.event.parentElement.children) { - e.style.pointerEvents = ""; + for (let e of that.event.parentElement!.children) { + (e as HTMLElement).style.pointerEvents = ""; } // place_in_edit_mode(that.event); let localevent = that.event; - that.event = null; + that.event = false - callback (localevent); + // callback(localevent); } } } - + /* This incarnation of this function only adds the calendar switcher dropdown. All events are already editable by switching to that tab. @@ -259,14 +270,15 @@ class EventCreator { // tab.querySelector("input[name='summary']").focus(); // } - -window.addEventListener('load', function () { +declare let EDIT_MODE: boolean + +window.addEventListener('load', function() { // let start_time = document.querySelector("meta[name='start-time']").content; // let end_time = document.querySelector("meta[name='end-time']").content; const sch = new SmallcalCellHighlight( - document.querySelector('.small-calendar')) + document.querySelector('.small-calendar')!) const timebar = new Timebar(/*start_time, end_time*/); @@ -282,42 +294,45 @@ window.addEventListener('load', function () { if (true && EDIT_MODE) { let eventCreator = new EventCreator; for (let c of document.getElementsByClassName("events")) { - c.onmousedown = eventCreator.create_event_down(c); - c.onmousemove = eventCreator.create_event_move( - (c,e) => e.offsetY / c.clientHeight, + if (!(c instanceof HTMLElement)) continue; + c.addEventListener('mousedown', eventCreator.create_event_down(c)); + c.addEventListener('mousemove', eventCreator.create_event_move( + (c, e) => e.offsetY / c.clientHeight, /* every quarter, every hour */ - 1/(24*4), false - ); - c.onmouseup = eventCreator.create_event_finisher( - function (event) { - let popupElement = document.getElementById("popup" + event.id); - open_popup(popupElement); + 1 / (24 * 4), false + )); + c.addEventListener('mouseup', eventCreator.create_event_finisher( + function(event: VEvent) { + // let popupElement = document.getElementById("popup" + event.id); + // open_popup(popup_from_event(event)); - popupElement.querySelector("input[name='summary']").focus(); + // popupElement.querySelector("input[name='summary']").focus(); - }); + })); } for (let c of document.getElementsByClassName("longevents")) { + if (!(c instanceof HTMLElement)) continue; c.onmousedown = eventCreator.create_event_down(c); c.onmousemove = eventCreator.create_event_move( - (c,e) => e.offsetX / c.clientWidth, + (c, e) => e.offsetX / c.clientWidth, /* every day, NOTE should be changed to check interval of longevents */ - 1/7, true + 1 / 7, true ); c.onmouseup = eventCreator.create_event_finisher( - function (event) { - let popupElement = document.getElementById("popup" + event.id); - open_popup(popupElement); + function(event) { + // TODO restore this + // 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(); + // /* 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(); }); } @@ -332,7 +347,7 @@ window.addEventListener('load', function () { On mobile they also have the problem that they make the whole page scroll there. */ - el.parentElement.removeAttribute("href"); + el.parentElement!.removeAttribute("href"); let popup = document.getElementById("popup" + el.id); // popup.getElementsByClassName("edit-form")[0].onsubmit = function () { @@ -349,12 +364,12 @@ window.addEventListener('load', function () { } - document.onkeydown = function (evt) { - evt = evt || window.event; - if (! evt.key) return; - if (evt.key.startsWith("Esc")) { - close_all_popups(); - } + document.onkeydown = function(evt) { + evt = evt || window.event; + if (!evt.key) return; + if (evt.key.startsWith("Esc")) { + close_all_popups(); + } } @@ -364,20 +379,21 @@ window.addEventListener('load', function () { form updates. */ - let gotodatebtn = document.querySelector("#jump-to .btn"); + let gotodatebtn = document.querySelector("#jump-to .btn")!; let target_href = (new Date).format("~Y-~m-~d") + ".html"; let golink = makeElement('a', { className: 'btn', href: target_href, innerHTML: gotodatebtn.innerHTML, - }); + }) as HTMLAnchorElement gotodatebtn.replaceWith(golink); - document.querySelector("#jump-to input[name='date']").onchange = function () { - let date = this.valueAsDate.format("~Y-~m-~d"); - console.log(date); - golink.href = date + ".html"; - } + (document.querySelector("#jump-to input[name='date']") as HTMLInputElement) + .onchange = function() { + let date = (this as HTMLInputElement).valueAsDate!.format("~Y-~m-~d"); + console.log(date); + golink.href = date + ".html"; + } /* ---------------------------------------- */ @@ -386,26 +402,27 @@ window.addEventListener('load', function () { Before init_input_list since we need this listener to be propagated to clones. [CATEGORIES_BIND] */ - for (let lst of document.querySelectorAll(".input-list[data-property='categories']")) { - let f = function () { - console.log(lst, lst.closest('.popup-container')); - let event = event_from_popup(lst.closest('.popup-container')) - event.properties.categories = lst.get_value(); - }; - - for (let inp of lst.querySelectorAll('input')) { - inp.addEventListener('input', f); - } - } + // TODO fix this + // for (let lst of document.querySelectorAll(".input-list[data-property='categories']")) { + // let f = function() { + // console.log(lst, lst.closest('.popup-container')); + // let event = event_from_popup(lst.closest('.popup-container')) + // event.properties.categories = lst.get_value(); + // }; + + // for (let inp of lst.querySelectorAll('input')) { + // inp.addEventListener('input', f); + // } + // } // init_arbitary_kv(); // init_input_list(); - document.addEventListener('keydown', function (event) { + document.addEventListener('keydown', function(event) { if (event.key == '/') { - let searchbox = document.querySelector('.simplesearch [name=q]') + let searchbox = document.querySelector('.simplesearch [name=q]') as HTMLInputElement // focuses the input, and selects all the text in it searchbox.select(); event.preventDefault(); diff --git a/static/server_connect.ts b/static/server_connect.ts index ef5de5a9..a6599500 100644 --- a/static/server_connect.ts +++ b/static/server_connect.ts @@ -1,17 +1,22 @@ -async function remove_event (element) { - let uid = element.querySelector("icalendar uid text").textContent; +/* +async function remove_event(element: Element): void { + let uidElement = element.querySelector("icalendar uid text") + if (uidElement === null) { + throw "Element lacks uid, giving up" + } + let uid: uid = uidElement.textContent!; let data = new URLSearchParams(); data.append('uid', uid); - let response = await fetch ( '/remove', { + let response = await fetch('/remove', { method: 'POST', body: data }); console.log(response); - toggle_popup("popup" + element.id); + toggle_popup(popup_from_event(element)); if (response.status < 200 || response.status >= 300) { let body = await response.text(); @@ -20,28 +25,29 @@ async function remove_event (element) { element.remove(); } } - -function event_to_jcal (event) { - /* 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) { +*/ + +// function event_to_jcal(event) { +// /* 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: VEvent) { // let xml = event.getElementsByTagName("icalendar")[0].outerHTML - let calendar = event.properties.calendar.value; + let calendar = event.getProperty('x-hnh-calendar-name'); console.log('calendar=', calendar/*, xml*/); @@ -51,11 +57,9 @@ async function create_event (event) { console.log(event); - let jcal = event_to_jcal(event); + let jcal = event.to_jcal(); - - - let doc = jcal_to_xcal(jcal); + let doc: Document = jcal_to_xcal(jcal); console.log(doc); let str = doc.documentElement.outerHTML; console.log(str); @@ -65,7 +69,7 @@ async function create_event (event) { // return; - let response = await fetch ( '/insert', { + let response = await fetch('/insert', { method: 'POST', body: data }); @@ -91,18 +95,19 @@ async function create_event (event) { .parseFromString(body, 'text/xml') .children[0]; - let child; - while ((child = return_properties.firstChild)) { - let target = event.querySelector( - "vevent properties " + child.tagName); - if (target) { - target.replaceWith(child); - } else { - event.querySelector("vevent properties") - .appendChild(child); - } - } + // let child; + // while ((child = return_properties.firstChild)) { + // let target = event.querySelector( + // "vevent properties " + child.tagName); + // if (target) { + // target.replaceWith(child); + // } else { + // event.querySelector("vevent properties") + // .appendChild(child); + // } + // } + + // event.classList.remove("generated"); + // toggle_popup(popup_from); - event.classList.remove("generated"); - toggle_popup("popup" + event.id); } diff --git a/static/types.ts b/static/types.ts index 02ae2261..7e10e90e 100644 --- a/static/types.ts +++ b/static/types.ts @@ -15,6 +15,22 @@ let all_types = [ 'boolean', /* boolean */ ] +type ical_type + = 'text' + | 'uri' + | 'binary' + | 'float' + | 'integer' + | 'date-time' + | 'date' + | 'duration' + | 'period' + | 'utc-offset' + | 'cal-address' + | 'recur' + | 'boolean' + | 'unknown' + let property_names = [ 'calscale', 'method', 'prodid', 'version', 'attach', 'categories', 'class', 'comment', 'description', 'geo', 'location', 'percent-complete', @@ -26,84 +42,140 @@ let property_names = [ ]; -let valid_fields = { - 'VCALENDAR': ['PRODID', 'VERSION', 'CALSCALE', 'METHOD'], - 'VEVENT': ['DTSTAMP', 'UID', 'DTSTART', 'CLASS', 'CREATED', - 'DESCRIPTION', 'GEO', 'LAST-MODIFIED', 'LOCATION', - 'ORGANIZER', 'PRIORITY', 'SEQUENCE', 'STATUS', - 'SUMMARY', 'TRANSP', 'URL', 'RECURRENCE-ID', - 'RRULE', 'DTEND', 'DURATION', 'ATTACH', 'ATTENDEE', - 'CATEGORIES', 'COMMENT', 'CONTACT', 'EXDATE', - 'REQUEST-STATUS', 'RELATED-TO', 'RESOURCES', 'RDATE'], - 'VTODO': ['DTSTAMP', 'UID', 'CLASS', 'COMPLETED', 'CREATED', - 'DESCRIPTION', 'DTSTART', 'GEO', 'LAST-MODIFIED', - 'LOCATION', 'ORGANIZER', 'PERCENT-COMPLETE', 'PRIORITY', - 'RECURRENCE-ID', 'SEQUENCE', 'STATUS', 'SUMMARY', 'URL', - 'RRULE', 'DUE', 'DURATION', 'ATTACH', 'ATTENDEE', 'CATEGORIES', - 'COMMENT', 'CONTACT', 'EXDATE', 'REQUEST-STATUS', 'RELATED-TO', - 'RESOURCES', 'RDATE',], - 'VJOURNAL': ['DTSTAMP', 'UID', 'CLASS', 'CREATED', 'DTSTART', 'LAST-MODIFIED', - 'ORGANIZER', 'RECURRENCE-ID', 'SEQUENCE', 'STATUS', 'SUMMARY', - 'URL', 'RRULE', 'ATTACH', 'ATTENDEE', 'CATEGORIES', 'COMMENT', - 'CONTACT', 'DESCRIPTION', 'EXDATE', 'RELATED-TO', 'RDATE', - 'REQUEST-STATUS'], - 'VFREEBUSY': ['DTSTAMP', 'UID', 'CONTACT', 'DTSTART', 'DTEND', - 'ORGANIZER', 'URL', 'ATTENDEE', 'COMMENT', 'FREEBUSY', - 'REQUEST-STATUS'], - 'VTIMEZONE': ['TZID', 'LAST-MODIFIED', 'TZURL'], - 'VALARM': ['ACTION', 'TRIGGER', 'DURATION', 'REPEAT', 'ATTACH', - 'DESCRIPTION', 'SUMMARY', 'ATTENDEE'], - 'STANDARD': ['DTSTART', 'TZOFFSETFROM', 'TZOFFSETTO', 'RRULE', - 'COMMENT', 'RDATE', 'TZNAME'], -}; +let valid_fields: Map = new Map([ + ['VCALENDAR', ['PRODID', 'VERSION', 'CALSCALE', 'METHOD']], + ['VEVENT', ['DTSTAMP', 'UID', 'DTSTART', 'CLASS', 'CREATED', + 'DESCRIPTION', 'GEO', 'LAST-MODIFIED', 'LOCATION', + 'ORGANIZER', 'PRIORITY', 'SEQUENCE', 'STATUS', + 'SUMMARY', 'TRANSP', 'URL', 'RECURRENCE-ID', + 'RRULE', 'DTEND', 'DURATION', 'ATTACH', 'ATTENDEE', + 'CATEGORIES', 'COMMENT', 'CONTACT', 'EXDATE', + 'REQUEST-STATUS', 'RELATED-TO', 'RESOURCES', 'RDATE']], + ['VTODO', ['DTSTAMP', 'UID', 'CLASS', 'COMPLETED', 'CREATED', + 'DESCRIPTION', 'DTSTART', 'GEO', 'LAST-MODIFIED', + 'LOCATION', 'ORGANIZER', 'PERCENT-COMPLETE', 'PRIORITY', + 'RECURRENCE-ID', 'SEQUENCE', 'STATUS', 'SUMMARY', 'URL', + 'RRULE', 'DUE', 'DURATION', 'ATTACH', 'ATTENDEE', 'CATEGORIES', + 'COMMENT', 'CONTACT', 'EXDATE', 'REQUEST-STATUS', 'RELATED-TO', + 'RESOURCES', 'RDATE',]], + ['VJOURNAL', ['DTSTAMP', 'UID', 'CLASS', 'CREATED', 'DTSTART', 'LAST-MODIFIED', + 'ORGANIZER', 'RECURRENCE-ID', 'SEQUENCE', 'STATUS', 'SUMMARY', + 'URL', 'RRULE', 'ATTACH', 'ATTENDEE', 'CATEGORIES', 'COMMENT', + 'CONTACT', 'DESCRIPTION', 'EXDATE', 'RELATED-TO', 'RDATE', + 'REQUEST-STATUS']], + ['VFREEBUSY', ['DTSTAMP', 'UID', 'CONTACT', 'DTSTART', 'DTEND', + 'ORGANIZER', 'URL', 'ATTENDEE', 'COMMENT', 'FREEBUSY', + 'REQUEST-STATUS']], + ['VTIMEZONE', ['TZID', 'LAST-MODIFIED', 'TZURL']], + ['VALARM', ['ACTION', 'TRIGGER', 'DURATION', 'REPEAT', 'ATTACH', + 'DESCRIPTION', 'SUMMARY', 'ATTENDEE']], + ['STANDARD', ['DTSTART', 'TZOFFSETFROM', 'TZOFFSETTO', 'RRULE', + 'COMMENT', 'RDATE', 'TZNAME']], +]) + +valid_fields.set('DAYLIGHT', valid_fields.get('STANDARD')!); + +type known_ical_types + = 'ACTION' + | 'ATTACH' + | 'ATTENDEE' + | 'CALSCALE' + | 'CATEGORIES' + | 'CLASS' + | 'COMMENT' + | 'COMPLETED' + | 'CONTACT' + | 'CREATED' + | 'DESCRIPTION' + | 'DTEND' + | 'DTSTAMP' + | 'DTSTART' + | 'DUE' + | 'DURATION' + | 'EXDATE' + | 'FREEBUSY' + | 'GEO' + | 'LAST-MODIFIED' + | 'LOCATION' + | 'METHOD' + | 'ORGANIZER' + | 'PERCENT-COMPLETE' + | 'PRIORITY' + | 'PRODID' + | 'RDATE' + | 'RECURRENCE-ID' + | 'RELATED-TO' + | 'REPEAT' + | 'REQUEST-STATUS' + | 'RESOURCES' + | 'RRULE' + | 'SEQUENCE' + | 'STATUS' + | 'SUMMARY' + | 'TRANSP' + | 'TRIGGER' + | 'TZID' + | 'TZNAME' + | 'TZOFFSETFROM' + | 'TZOFFSETTO' + | 'TZURL' + | 'URL' + | 'VERSION' + +let valid_input_types: Map = + new Map([ + ['ACTION', ['text']], // AUDIO|DISPLAY|EMAIL|*other* + ['ATTACH', ['uri', 'binary']], + ['ATTENDEE', ['cal-address']], + ['CALSCALE', ['text']], + ['CATEGORIES', [['text']]], + ['CLASS', ['text']], // PUBLIC|PRIVATE|CONFIDENTIAL|*other* + ['COMMENT', ['text']], + ['COMPLETED', ['date-time']], + ['CONTACT', ['text']], + ['CREATED', ['date-time']], + ['DESCRIPTION', ['text']], + ['DTEND', ['date', 'date-time']], + ['DTSTAMP', ['date-time']], + ['DTSTART', ['date', 'date-time']], + ['DUE', ['date', 'date-time']], + ['DURATION', ['duration']], + ['EXDATE', [['date', 'date-time']]], + ['FREEBUSY', [['period']]], + ['GEO', ['float']], // pair of floats + ['LAST-MODIFIED', ['date-time']], + ['LOCATION', ['text']], + ['METHOD', ['text']], + ['ORGANIZER', ['cal-address']], + ['PERCENT-COMPLETE', ['integer']], // 0-100 + ['PRIORITY', ['integer']], // 0-9 + ['PRODID', ['text']], + ['RDATE', [['date', 'date-time', 'period']]], + ['RECURRENCE-ID', ['date', 'date-time']], + ['RELATED-TO', ['text']], + ['REPEAT', ['integer']], + ['REQUEST-STATUS', ['text']], + ['RESOURCES', [['text']]], + ['RRULE', ['recur']], + ['SEQUENCE', ['integer']], + ['STATUS', ['text']], // see 3.8.1.11 + ['SUMMARY', ['text']], + ['TRANSP', ['text']], // OPAQUE|TRANSPARENT + ['TRIGGER', ['duration', 'date-time']], + ['TZID', ['text']], + ['TZNAME', ['text']], + ['TZOFFSETFROM', ['utc-offset']], + ['TZOFFSETTO', ['utc-offset']], + ['TZURL', ['uri']], + ['URL', ['uri']], + ['VERSION', ['text']], + ]) as Map + +// type JCalLine { +// } -valid_fields['DAYLIGHT'] = valid_fields['STANDARD']; +type tagname = 'vevent' | string +type JCalProperty = [string, any, ical_type, any[]] -let valid_input_types = { - 'ACTION': ['text'], // AUDIO|DISPLAY|EMAIL|*other* - 'ATTACH': ['uri', 'binary'], - 'ATTENDEE': ['cal-address'], - 'CALSCALE': ['text'], - 'CATEGORIES': [['text']], - 'CLASS': ['text'], // PUBLIC|PRIVATE|CONFIDENTIAL|*other* - 'COMMENT': ['text'], - 'COMPLETED': ['date-time'], - 'CONTACT': ['text'], - 'CREATED': ['date-time'], - 'DESCRIPTION': ['text'], - 'DTEND': ['date', 'date-time'], - 'DTSTAMP': ['date-time'], - 'DTSTART': ['date', 'date-time'], - 'DUE': ['date', 'date-time'], - 'DURATION': ['duration'], - 'EXDATE': [['date', 'date-time']], - 'FREEBUSY': [['period']], - 'GEO': ['float'], // pair of floats - 'LAST-MODIFIED': ['date-time'], - 'LOCATION': ['text'], - 'METHOD': ['text'], - 'ORGANIZER': ['cal-address'], - 'PERCENT-COMPLETE': ['integer'], // 0-100 - 'PRIORITY': ['integer'], // 0-9 - 'PRODID': ['text'], - 'RDATE': [['date', 'date-time', 'period']], - 'RECURRENCE-ID': ['date', 'date-time'], - 'RELATED-TO': ['text'], - 'REPEAT': ['integer'], - 'REQUEST-STATUS': ['text'], - 'RESOURCES': [['text']], - 'RRULE': ['recur'], - 'SEQUENCE': ['integer'], - 'STATUS': ['text'], // see 3.8.1.11 - 'SUMMARY': ['text'], - 'TRANSP': ['text'], // OPAQUE|TRANSPARENT - 'TRIGGER': ['duration', 'date-time'], - 'TZID': ['text'], - 'TZNAME': ['text'], - 'TZOFFSETFROM': ['utc-offset'], - 'TZOFFSETTO': ['utc-offset'], - 'TZURL': ['uri'], - 'URL': ['uri'], - 'VERSION': ['text'], -} +type JCal = [tagname, JCalProperty[], JCal[]] diff --git a/static/vevent.ts b/static/vevent.ts index 678f2134..72ab28b1 100644 --- a/static/vevent.ts +++ b/static/vevent.ts @@ -1,46 +1,55 @@ "use strict"; +interface Redrawable extends HTMLElement { + redraw: ((data: VEvent) => void) +} + class VEventValue { - constructor (type, value, parameters = {}) { + + type: ical_type + value: any + parameters: Map + + constructor(type: ical_type, value: any, parameters = new Map()) { this.type = type; this.value = value; this.parameters = parameters; } - to_jcal () { + to_jcal(): [Map, ical_type, any] { 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; + 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.parameters, this.type, value]; } @@ -51,24 +60,38 @@ class VEventDuration extends VEventValue { } class VEvent { - constructor (properties = {}, components = []) { + + properties: Map + components: VEvent[] + registered: Redrawable[] + + constructor(properties: Map = new Map(), components: VEvent[] = []) { this.properties = properties; this.components = components; this.registered = []; } - getProperty (key) { - let e = this.properties[key]; - if (! e) return e; + getProperty(key: string): any | undefined { + let e = this.properties.get(key); + if (!e) return e; return e.value; } - setProperty (key, value) { - let e = this.properties[key]; - if (! e) { - let type = (valid_input_types[key.toUpperCase()] || ['unknown'])[0] - if (typeof type === typeof []) type = type[0]; - e = this.properties[key] = new VEventValue(type, value); + setProperty(key: string, value: any) { + let e = this.properties.get(key); + if (!e) { + key = key.toUpperCase() + let type: ical_type + let type_ = valid_input_types.get(key) + if (type_ === undefined) { + type = 'unknown' + } else if (type_ instanceof Array) { + type = type_[0] + } else { + type = type_ + } + e = new VEventValue(type, value) + this.properties.set(key, e); } else { e.value = value; } @@ -79,11 +102,11 @@ class VEvent { } } - register (htmlNode) { + register(htmlNode: Redrawable) { this.registered.push(htmlNode); } - to_jcal () { + to_jcal(): JCal { let out_properties = [] for (let [key, value] of Object.entries(this.properties)) { let sub = value.to_jcal(); @@ -94,118 +117,119 @@ class VEvent { } } -function make_vevent_value (value_tag) { +function make_vevent_value(value_tag: Element) { /* TODO parameters */ - return new VEventValue (value_tag.tagName, make_vevent_value_ (value_tag)); + return new VEventValue( + /* TODO error on invalid type? */ + value_tag.tagName as ical_type, + make_vevent_value_(value_tag)); } -function make_vevent_value_ (value_tag) { +function make_vevent_value_(value_tag: Element) { /* RFC6321 3.6. */ switch (value_tag.tagName) { - case 'binary': - /* Base64 to binary - Seems to handle inline whitespace, which xCal standard reqires - */ - return atob(value_tag.innerHTML) - break; - - case 'boolean': - switch (value_tag.innerHTML) { - case 'true': return true; - case 'false': return false; - default: - console.warn(`Bad boolean ${value_tag.innerHTML}, defaulting with !!`) - return !! value_tag.innerHTML; - } - break; - - case 'time': - case 'date': - case 'date-time': - return parseDate(value_tag.innerHTML); - break; - - case 'duration': - /* TODO duration parser here 'P1D' */ - return value_tag.innerHTML; - break; - - case 'float': - case 'integer': - return +value_tag.innerHTML; - break; - - case 'period': - /* TODO has sub components, meaning that a string wont do */ - let start = value_tag.getElementsByTagName('start')[0] - parseDate(start.innerHTML); - let other; - if ((other = value_tag.getElementsByTagName('end')[0])) { - return parseDate(other.innerHTML) - } else if ((other = value_tag.getElementsByTagName('duration')[0])) { - /* TODO parse duration */ - return other.innerHTML - } else { - console.warn('Invalid end to period, defaulting to 1H'); - return new Date(3600); - } + case 'binary': + /* Base64 to binary + Seems to handle inline whitespace, which xCal standard reqires + */ + return atob(value_tag.innerHTML) + + case 'boolean': + switch (value_tag.innerHTML) { + case 'true': return true; + case 'false': return false; + default: + console.warn(`Bad boolean ${value_tag.innerHTML}, defaulting with !!`) + return !!value_tag.innerHTML; + } + + case 'time': + case 'date': + case 'date-time': + return parseDate(value_tag.innerHTML); + + case 'duration': + /* TODO duration parser here 'P1D' */ + return value_tag.innerHTML; + + case 'float': + case 'integer': + return +value_tag.innerHTML; + + case 'period': + /* TODO has sub components, meaning that a string wont do */ + let start = value_tag.getElementsByTagName('start')[0] + parseDate(start.innerHTML); + let other; + if ((other = value_tag.getElementsByTagName('end')[0])) { + return parseDate(other.innerHTML) + } else if ((other = value_tag.getElementsByTagName('duration')[0])) { + /* TODO parse duration */ + return other.innerHTML + } else { + console.warn('Invalid end to period, defaulting to 1H'); + return new Date(3600); + } - case 'recur': - /* TODO parse */ - return ""; + case 'recur': + /* TODO parse */ + return ""; - case 'utc-offset': - /* TODO parse */ - return ""; + case 'utc-offset': + /* TODO parse */ + return ""; - default: - console.warn(`Unknown type '${value_tag.tagName}', defaulting to string`) - case 'cal-address': - case 'uri': - case 'text': - return value_tag.innerHTML; + default: + console.warn(`Unknown type '${value_tag.tagName}', defaulting to string`) + case 'cal-address': + case 'uri': + case 'text': + return value_tag.innerHTML; } } /* xml dom object -> class VEvent */ -function xml_to_vcal (xml) { +function xml_to_vcal(xml: Element): VEvent { /* xml MUST have a VEVENT (or equivalent) as its root */ let properties = xml.getElementsByTagName('properties')[0]; let components = xml.getElementsByTagName('components')[0]; - let property_map = {} + let property_map = new Map() if (properties) { for (var i = 0; i < properties.childElementCount; i++) { let tag = properties.childNodes[i]; + if (!(tag instanceof Element)) continue; let parameters = {}; - let value = []; + let value: VEventValue | VEventValue[] = []; for (var j = 0; j < tag.childElementCount; j++) { let child = tag.childNodes[j]; + if (!(child instanceof Element)) continue; switch (tag.tagName) { - case 'parameters': - parameters = /* handle parameters */ {}; - break; + case 'parameters': + parameters = /* TODO handle parameters */ {}; + break; /* These can contain multiple value tags, per RFC6321 3.4.1.1. */ - case 'categories': - case 'resources': - case 'freebusy': - case 'exdate': - case 'rdate': - value.push(make_vevent_value(child)); - break; - default: - value = make_vevent_value(child); + case 'categories': + case 'resources': + case 'freebusy': + case 'exdate': + case 'rdate': + (value as VEventValue[]).push(make_vevent_value(child)); + break; + default: + value = make_vevent_value(child); } } - property_map[tag.tagName] = value; + property_map.set(tag.tagName, value); } } let component_list = [] if (components) { for (let child of components.childNodes) { + if (!(child instanceof Element)) continue; component_list.push(xml_to_vcal(child)) } } -- cgit v1.2.3