From 1df15b2ceaef09b48a39aa6046b577da11ea2f72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Fri, 26 Nov 2021 15:32:41 +0100 Subject: Got categories working. --- module/calp/html/vcomponent.scm | 8 +++ module/vcomponent/xcal/parse.scm | 43 ++++++++++++--- static/components/input-list.ts | 101 +++++++++++++++++++++++++++------- static/components/vevent-edit.ts | 8 ++- static/types.ts | 8 +-- static/vevent.ts | 114 ++++++++++++++++++++++++++++++--------- 6 files changed, 225 insertions(+), 57 deletions(-) diff --git a/module/calp/html/vcomponent.scm b/module/calp/html/vcomponent.scm index 23884b58..8a301e04 100644 --- a/module/calp/html/vcomponent.scm +++ b/module/calp/html/vcomponent.scm @@ -265,6 +265,14 @@ ; ,(prop ev 'DESCRIPTION) )) + ,@(with-label + "Kategorier" + `(input-list + (@ (name "categories") + (data-property "categories")) + (input (@ (type "text") + (placeholder "Kattegori"))))) + ;; ,@(with-label ;; "Kategorier" ;; ;; It would be better if these input-list's worked on the same diff --git a/module/vcomponent/xcal/parse.scm b/module/vcomponent/xcal/parse.scm index 124a91f4..48ce301e 100644 --- a/module/vcomponent/xcal/parse.scm +++ b/module/vcomponent/xcal/parse.scm @@ -171,11 +171,25 @@ ;; ignore empty fields ;; mostly for (unless (null? value) - (set! (prop* component tag*) - (make-vline tag* - (handle-tag - tag (handle-value type params value)) - params)))))] + (let () + (define vline + (make-vline tag* + (handle-tag + tag (handle-value type params value)) + params)) + (if (memv tag* '(ATTACH ATTENDEE CATEGORIES + COMMENT CONTACT EXDATE + REQUEST-STATUS RELATED-TO + RESOURCES RDATE + ;; x-prop + ;; iana-prop + )) + (aif (prop* component tag*) + (set! (prop* component tag*) (cons vline it)) + (set! (prop* component tag*) (list vline))) + ;; else + (set! (prop* component tag*) vline)) + ))))] [(tag (type value ...) ...) (for (type value) in (zip type value) @@ -184,7 +198,7 @@ (unless (null? value) (let ((params (make-hash-table)) (tag* (symbol-upcase tag))) - (set! (prop* component tag*) + (define vline (make-vline tag* (handle-tag tag (let ((v (handle-value type params value))) @@ -192,7 +206,22 @@ (if (eq? tag 'categories) (string-split v #\,) v))) - params)))))]))) + params)) + ;; + + (if (memv tag* '(ATTACH ATTENDEE CATEGORIES + COMMENT CONTACT EXDATE + REQUEST-STATUS RELATED-TO + RESOURCES RDATE + ;; x-prop + ;; iana-prop + )) + (aif (prop* component tag*) + (set! (prop* component tag*) (cons vline it)) + (set! (prop* component tag*) (list vline))) + ;; else + (set! (prop* component tag*) vline)) + )))]))) ;; children (awhen (assoc-ref sxcal 'components) diff --git a/static/components/input-list.ts b/static/components/input-list.ts index 326cb2b5..899e8f4f 100644 --- a/static/components/input-list.ts +++ b/static/components/input-list.ts @@ -2,11 +2,15 @@ export { InputList } /* This file replaces input_list.js */ +/* + TODO allow each item to be a larger unit, possibly containing multiple input + fields. +*/ class InputList extends HTMLElement { el: HTMLInputElement; - values: [HTMLInputElement, any][] = []; + _listeners: [string, (e: Event) => void][] = []; constructor() { super(); @@ -14,20 +18,27 @@ class InputList extends HTMLElement { } connectedCallback() { + for (let child of this.children) { + child.remove(); + } this.addInstance(); } - addInstance() { + createInstance(): HTMLInputElement { let new_el = this.el.cloneNode(true) as HTMLInputElement let that = this; new_el.addEventListener('input', function() { + /* TODO .value is empty both if it's actually empty, but also + for invalid input. Check new_el.validity, and new_el.validationMessage + */ if (new_el.value === '') { - let sibling = this.previousElementSibling || this.nextElementSibling; - // this.remove(); - // that.values = that.values.filter((p) => p[0] == this) - that.values = that.values.filter((p) => p[0] != this); - this.remove(); - (sibling as HTMLInputElement).focus(); + let sibling = (this.previousElementSibling || this.nextElementSibling) + /* Only remove ourselves if we have siblings + Otherwise we just linger */ + if (sibling) { + this.remove(); + (sibling as HTMLInputElement).focus(); + } } else { if (!this.nextElementSibling) { that.addInstance(); @@ -36,24 +47,76 @@ class InputList extends HTMLElement { } } }); - this.values.push([new_el, '']) - // this.appendChild(new_el); - this.replaceChildren(... this.values.map((p) => p[0])) + + for (let [type, proc] of this._listeners) { + new_el.addEventListener(type, proc); + } + + return new_el; + } + + addInstance() { + let new_el = this.createInstance(); + this.appendChild(new_el); } get value(): any[] { - return [] + let value_list = [] + for (let child of this.children) { + value_list.push((child as any).value); + } + if (value_list[value_list.length - 1] === '') { + value_list.pop(); + } + return value_list } set value(new_value: any[]) { - let els = []; + + let all_equal = true; + for (let i = 0; i < this.children.length; i++) { + let sv = (this.children[i] as any).value + all_equal + &&= (sv == new_value[i]) + || (sv === '' && new_value[i] == undefined) + } + if (all_equal) return; + + /* Copy our current input elements into a dictionary. + This allows us to only create new elements where needed + */ + let values = new Map; + for (let child of this.children) { + values.set((child as HTMLInputElement).value, child); + } + + let output_list: HTMLInputElement[] = [] for (let value of new_value) { - let new_el = this.el.cloneNode() as HTMLInputElement; - new_el.value = value; - els.push(new_el); + let element; + /* Only create element if needed */ + if ((element = values.get(value))) { + output_list.push(element) + /* clear dictionary */ + values.set(value, false); + } else { + let new_el = this.createInstance(); + new_el.value = value; + output_list.push(new_el); + } + } + /* final, trailing, element */ + output_list.push(this.createInstance()); + + this.replaceChildren(...output_list); + } + + addEventListener(type: string, proc: ((e: Event) => void)) { + // if (type != 'input') throw "Only input supported"; + + this._listeners.push([type, proc]) + + for (let child of this.children) { + child.addEventListener(type, proc); } - /* Final element (empty) */ - els.push(this.el.cloneNode() as HTMLInputElement); - this.replaceChildren(...els); } } diff --git a/static/components/vevent-edit.ts b/static/components/vevent-edit.ts index 4408cbb8..b9b733a0 100644 --- a/static/components/vevent-edit.ts +++ b/static/components/vevent-edit.ts @@ -1,9 +1,10 @@ export { ComponentEdit } import { ComponentVEvent } from './vevent' +import { InputList } from './input-list' import { DateTimeInput } from './date-time-input' -import { vcal_objects, event_calendar_mapping } from '../globals' +import { vcal_objects } from '../globals' import { VEvent } from '../vevent' import { create_event } from '../server_connect' @@ -55,18 +56,21 @@ class ComponentEdit extends ComponentVEvent { // for (let el of this.getElementsByClassName("interactive")) { for (let el of this.querySelectorAll("[data-property]")) { // console.log(el); - el.addEventListener('input', () => { + el.addEventListener('input', (e) => { let obj = vcal_objects.get(this.uid) + console.log(el, e); if (obj === undefined) { throw 'No object with uid ' + this.uid } if (!(el instanceof HTMLInputElement || el instanceof DateTimeInput || el instanceof HTMLTextAreaElement + || el instanceof InputList )) { console.log(el, 'not an HTMLInputElement'); return; } + // console.log(`obj[${el.dataset.property!}] = `, el.value); obj.setProperty( el.dataset.property!, el.value) diff --git a/static/types.ts b/static/types.ts index 2c26308e..567b9a95 100644 --- a/static/types.ts +++ b/static/types.ts @@ -129,7 +129,7 @@ type known_ical_types | 'URL' | 'VERSION' -let valid_input_types: Map = +let valid_input_types: Map> = new Map([ ['ACTION', ['text']], // AUDIO|DISPLAY|EMAIL|*other* ['ATTACH', ['uri', 'binary']], @@ -178,7 +178,7 @@ let valid_input_types: Map = ['UID', ['text']], ['URL', ['uri']], ['VERSION', ['text']], - ]) as Map + ]) // type JCalLine { // } @@ -191,7 +191,9 @@ type uid = string What really are valid values for any? Does that depend on ical_type? Why is the tail a list? What really is the type for the parameter map? */ -type JCalProperty = [string, Map, ical_type, any[]] +type JCalProperty + = [string, Record, ical_type, any] + | [string, Record, ical_type, ...any[]] type JCal = [tagname, JCalProperty[], JCal[]] diff --git a/static/vevent.ts b/static/vevent.ts index 9bfd8dcf..12d8267f 100644 --- a/static/vevent.ts +++ b/static/vevent.ts @@ -14,6 +14,9 @@ interface Redrawable extends HTMLElement { class VEventValue { type: ical_type + + /* value should NEVER be a list, since multi-valued properties should + be split into multiple VEventValue objects! */ value: any parameters: Map @@ -23,12 +26,13 @@ class VEventValue { this.parameters = parameters; } - to_jcal(): [Map, ical_type, any] { + to_jcal(): [Record, ical_type, any] { let value; let v = this.value; switch (this.type) { case 'binary': /* TOOD */ + value = 'BINARY DATA GOES HERE'; break; case 'date-time': value = v.format("~Y-~m-~dT~H:~M:~S"); @@ -39,12 +43,15 @@ class VEventValue { break; case 'duration': /* TODO */ + value = 'DURATION GOES HERE'; break; case 'period': /* TODO */ + value = 'PERIOD GOES HERE'; break; case 'utc-offset': /* TODO */ + value = 'UTC-OFFSET GOES HERE'; break; case 'recur': value = v.asJcal(); @@ -58,7 +65,8 @@ class VEventValue { case 'boolean': value = v; } - return [this.parameters, this.type, value]; + + return [this.parameters, this.type, value] } } @@ -66,6 +74,10 @@ class VEventValue { class VEventDuration extends VEventValue { } +type list_values + = 'categories' | 'resources' | 'freebusy' | 'exdate' | 'rdate' + | 'CATEGORIES' | 'RESOURCES' | 'FREEBUSY' | 'EXDATE' | 'RDATE'; + /* Abstract representation of a calendar event (or similar). All "live" calendar data in the frontend should live in an object of this type. @@ -73,7 +85,7 @@ All "live" calendar data in the frontend should live in an object of this type. class VEvent { /* Calendar properties */ - properties: Map + properties: Map /* Children (such as alarms for events) */ components: VEvent[] @@ -85,7 +97,10 @@ class VEvent { _calendar: string | null = null; - constructor(properties: Map = new Map(), components: VEvent[] = []) { + constructor( + properties: Map = new Map(), + components: VEvent[] = [] + ) { this.components = components; this.registered = []; /* Re-normalize all given keys to upper case. We could require @@ -98,10 +113,18 @@ class VEvent { } } - getProperty(key: string): any | undefined { + getProperty(key: list_values): any[] | undefined; + getProperty(key: string): any | undefined; + + // getProperty(key: 'categories'): string[] | undefined + + getProperty(key: string): any | any[] | undefined { key = key.toUpperCase() let e = this.properties.get(key); if (!e) return e; + if (Array.isArray(e)) { + return e.map(ee => ee.value) + } return e.value; } @@ -110,29 +133,52 @@ class VEvent { } __setPropertyInternal(key: string, value: any, type?: ical_type) { + function resolve_type(key: string, type?: ical_type): ical_type { + if (type) { + return type; + } else { + let type_options = valid_input_types.get(key) + if (type_options === undefined) { + type = 'unknown' + } else if (type_options.length == 0) { + type = 'unknown' + } else { + if (Array.isArray(type_options[0])) { + type = type_options[0][0] + } else { + type = type_options[0] + } + } + return type; + } + } + key = key.toUpperCase(); - let e = this.properties.get(key); - if (e) { - if (type) { e.type = type; } - e.value = value; + if (Array.isArray(value)) { + this.properties.set(key, + value.map(el => new VEventValue(resolve_type(key, type), el))) return; } - if (!type) { - let type_ = valid_input_types.get(key) - if (type_ === undefined) { - type = 'unknown' - } else if (type_ instanceof Array) { - type = type_[0] + let current = this.properties.get(key); + if (current) { + if (Array.isArray(current)) { } else { - type = type_ + if (type) { current.type = type; } + current.value = value; + return; } } - e = new VEventValue(type, value) - this.properties.set(key, e); + type = resolve_type(key, type); + let new_value = new VEventValue(type, value) + this.properties.set(key, new_value); } + setProperty(key: list_values, value: any[], type?: ical_type): void; + setProperty(key: string, value: any, type?: ical_type): void; + setProperty(key: string, value: any, type?: ical_type) { this.__setPropertyInternal(key, value, type); + for (let el of this.registered) { el.redraw(this); } @@ -167,12 +213,27 @@ class VEvent { let out_properties: JCalProperty[] = [] console.log(this.properties); for (let [key, value] of this.properties) { - let prop: JCalProperty = [ - key.toLowerCase(), - ...value.to_jcal(), - ] - out_properties.push(prop); + console.log("key = ", key, ", value = ", value); + if (Array.isArray(value)) { + if (value.length == 0) continue; + let mostly = value.map(v => v.to_jcal()) + let values = mostly.map(x => x[2]) + console.log("mostly", mostly) + out_properties.push([ + key.toLowerCase(), + mostly[0][0], + mostly[0][1], + ...values + ]) + } else { + let prop: JCalProperty = [ + key.toLowerCase(), + ...value.to_jcal(), + ] + out_properties.push(prop); + } } + return ['vevent', out_properties, [/* alarms go here*/]] } } @@ -209,7 +270,7 @@ class RecurrenceRule { bysetpos?: number[] wkst?: weekday - to_jcal() { + to_jcal(): Record { let obj: any = {} if (this.freq) obj['freq'] = this.freq; if (this.until) obj['until'] = this.until.format(this.until.dateonly @@ -386,7 +447,7 @@ function xml_to_vcal(xml: Element): VEvent { let properties = xml.getElementsByTagName('properties')[0]; let components = xml.getElementsByTagName('components')[0]; - let property_map = new Map() + let property_map: Map = new Map; if (properties) { property_loop: for (var i = 0; i < properties.childElementCount; i++) { @@ -394,7 +455,8 @@ function xml_to_vcal(xml: Element): VEvent { if (!(tag instanceof Element)) continue; let parameters = {}; let value: VEventValue | VEventValue[] = []; - value_loop: for (var j = 0; j < tag.childElementCount; j++) { + value_loop: + for (var j = 0; j < tag.childElementCount; j++) { let child = tag.childNodes[j]; if (!(child instanceof Element)) continue; if (child.tagName == 'parameters') { -- cgit v1.2.3