/* 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; this._slots = {} this._values = {} 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')) { console.log (this.get(e.dataset.property)); } let f = (s, v) => { console.log(s, v); s.innerHTML = v.format(s.dataset && s.dataset.fmt); }; this.get(e.dataset.property).push([e, f]); if (e.classList.contains('summary')) { console.log (this.get(e.dataset.property)); } // console.log("registreing", e, e.dataset.property, this); } } /* checkbox for whole day */ for (let field of ['dtstart', 'dtend']) { this.get(`--${field}-time`).push( [el, (el, v) => { let date = this[field]; if (v == '') return; let [h,m,s] = v.split(':') date.setHours(Number(h)); date.setMinutes(Number(m)); date.setSeconds(0); this[field] = date; }]) this.get(`--${field}-date`).push( [el, (el, v) => { let date = this[field]; if (v == '') return; let [y,m,d] = v.split('-') date.setYear(Number(y)/* - 1900*/); date.setMonth(Number(m) - 1); date.setDate(d); this[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. */ this.get(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) { this.ical_properties.add(property) // console.log("prop", property) Object.defineProperty( this, property, { get: function() { return this._values[property].value; }, set: function (value) { console.log("set", property, value); this._values[property].value = value; console.log(this._slots[property]); for (let [slot,updater] of this._slots[property]) { console.log(updater, slot); updater(slot, value); } }, }); } /* icalendar properties */ for (let child of el.querySelector("vevent > properties").children) { /* child ≡ ... */ 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 ≡ ... */ /* 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); 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('dtstart').push( [el.style, (s, v) => s[wide_event?'left':'top'] = 100 * (to_local(v.value) - start)/(end - start) + "%"]); } if (this.dtend) { // el.properties.dtend = parseDate(el.properties.dtend); this.get('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.value)-start)/(end-start)) + "%"]); } /* ---------- Calendar ------------------------------ */ if (! el.dataset.calendar) { el.dataset.calendar = "Unknown"; } // let calprop = get_property(el, 'calendar', el.dataset.calendar); let calprop = this.get('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 ------------------------------ */ } /* 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(field, default_value) { // let el = this.html_element; if (! this._slots[field]) { this._slots[field] = []; this._values[field] = default_value; } // 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) { let v = this[prop]; if (v !== undefined) { properties.push([prop] + v.to_jcal()); } } return ['vevent', properties, [/* alarms go here */]] } } 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; 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]; } }