From 327b322b9583f760cd02ddad7a2a8890df26cc8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Thu, 23 Jun 2022 01:39:08 +0200 Subject: work --- module/calp/html/vcomponent.scm | 36 +++----- module/calp/html/view/calendar.scm | 128 ++++++++++++++++------------ module/calp/html/view/calendar/month.scm | 7 +- module/calp/html/view/calendar/week.scm | 7 +- module/calp/server/routes.scm | 67 ++++++++++++++- module/vcomponent/util/instance/methods.scm | 110 ++++++++++++++---------- static/components/edit-rrule.ts | 12 +-- static/components/popup-element.ts | 13 ++- static/components/tab-group-element.ts | 11 +-- static/components/vevent-block.ts | 4 +- static/components/vevent-description.ts | 4 +- static/components/vevent-edit.ts | 25 ++++-- static/components/vevent.ts | 58 +++++++++++-- static/event-creator.ts | 2 +- static/script.ts | 28 ++++-- static/server_connect.ts | 5 +- static/vevent.ts | 29 +++++++ 17 files changed, 369 insertions(+), 177 deletions(-) diff --git a/module/calp/html/vcomponent.scm b/module/calp/html/vcomponent.scm index 1cee47a5..472a8c2b 100644 --- a/module/calp/html/vcomponent.scm +++ b/module/calp/html/vcomponent.scm @@ -112,7 +112,9 @@ (class ,(when (and (prop ev 'PARTSTAT) (eq? 'TENTATIVE (prop ev 'PARTSTAT))) " tentative ")) - (data-uid ,(output-uid ev))))) + ,@(when (repeating? ev) + `((data-instance ,(datetime->string (as-datetime (prop ev 'DTSTART)))))) + (data-uid ,(prop ev 'UID))))) (div (@ (class "vevent eventtext summary-tab")) (h3 ,(fmt-header (when (prop ev 'RRULE) @@ -293,7 +295,10 @@ extra-attributes `((id ,(html-id ev) "-block") (data-calendar ,(base64encode (or (prop (parent ev) 'NAME) "unknown"))) - (data-uid ,(output-uid ev)) + (data-uid ,(prop ev 'UID)) + ;; TODO write helper for this (re-occuring) case + ,@(when (repeating? ev) + `((data-instance ,(datetime->string (as-datetime (prop ev 'DTSTART)))))) (class "vevent event" ,(when (and (prop ev 'PARTSTAT) @@ -352,29 +357,6 @@ (prop event 'RRULE))))) -;; Return a unique identifier for a specific instance of an event. -;; Allows us to reference each instance of a repeating event separately -;; from any other -(define-public (output-uid event) - (string-concatenate - (cons - (prop event 'UID) - (when (repeating? event) - ;; TODO this will break if a UID already looks like this... - ;; Just using a pre-generated unique string would solve it, - ;; until someone wants to break us. Therefore, we just give - ;; up for now, until a proper solution can be devised. - (list "---" - ;; TODO Will this give us a unique identifier? - ;; Or can two events share UID along with start time - (datetime->string - (as-datetime (or - ;; TODO What happens if the parameter RANGE=THISANDFUTURE is set? - (prop event 'RECURRENCE-ID) - (prop event 'DTSTART))) - "~Y-~m-~dT~H:~M:~S")))))) - - (define (week-day-select args) `(select (@ ,@args) (option "-") @@ -476,7 +458,9 @@ ;; (hr) - (input (@ (type "submit"))) + (input (@ (type "submit") (data-key "this") (value "This"))) + (input (@ (type "submit") (data-key "this_future") (value "This & Future"))) + (input (@ (type "submit") (data-key "all") (value "All"))) )))) ;; description in sidebar / tab of popup diff --git a/module/calp/html/view/calendar.scm b/module/calp/html/view/calendar.scm index 8b7d8075..9e2992a4 100644 --- a/module/calp/html/view/calendar.scm +++ b/module/calp/html/view/calendar.scm @@ -2,7 +2,7 @@ :use-module (hnh util) :use-module (vcomponent) :use-module ((vcomponent datetime) - :select (events-between)) + :select (ev-timesxcal)) ) @@ -46,6 +51,35 @@ (define repo-url (make-parameter "https://git.hornquist.se/calp")) +;; Mapping showing which events belongs to which calendar, +;; on the form +;; (calendar (@ (key ,(base64-encode calendar-name))) +;; (li ,event-uid) ...) +(define (calendar-event-mapping events) + `(div (@ (style "display:none !important;") + (id "calendar-event-mapping")) + + ;; ,(for (calendar entries ...) in (group-by parent events) + ;; `(calendar (@ (key ,(base64encode (prop calendar 'NAME)))) + ;; ,@(map (lambda (uid) `(li ,uid)) + ;; (map (extract 'UID) entries)))) + + ,(let ((ht (make-hash-table))) + (for-each (lambda (event) + (define name (prop (parent event) 'NAME)) + (hash-set! ht name + (cons (prop event 'UID) + (hash-ref ht name '())))) + events) + + (hash-map->list + (lambda (key values) + `(calendar (@ (key ,(base64encode key))) + ,@(map (lambda (uid) `(li ,uid)) + values))) + ht)))) + + ;; TODO document what @var{render-calendar} is supposed to take and return. ;; Can at least note that @var{render-calendar} is strongly encouraged to include ;; (script "const VIEW='??';"), where ?? is replaced by the name of the view. @@ -362,57 +396,39 @@ window.default_calendar='~a';" SEQUENCE REQUEST-STATUS ))) - ,@(let* ( - (flat-events - ;; A simple filter-sorted-stream on event-overlaps? here fails. - ;; See tests/annoying-events.scm - (stream->list - (stream-filter - (lambda (ev) - ((@ (vcomponent datetime) event-overlaps?) - ev pre-start - (date+ post-end (date day: 1)))) - (stream-take-while (lambda (ev) (date< - (as-date (prop ev 'DTSTART)) - (date+ post-end (date day: 1)))) - events)))) - (repeating% regular (partition repeating? flat-events)) - (repeating - (for ev in repeating% - (define instance (copy-vcomponent ev)) - - (set! (prop instance 'UID) (output-uid instance)) - (delete-parameter! (prop* instance 'DTSTART) '-X-HNH-ORIGINAL) - (delete-parameter! (prop* instance 'DTEND) '-X-HNH-ORIGINAL) - - instance))) - - `( - ;; Mapping showing which events belongs to which calendar, - ;; on the form - ;; (calendar (@ (key ,(base64-encode calendar-name))) - ;; (li ,event-uid) ...) - (div (@ (style "display:none !important;") - (id "calendar-event-mapping")) - ,(let ((ht (make-hash-table))) - (for-each (lambda (event) - (define name (prop (parent event) 'NAME)) - (hash-set! ht name - (cons (prop event 'UID) - (hash-ref ht name '())))) - (append regular repeating)) - - (hash-map->list - (lambda (key values) - `(calendar (@ (key ,(base64encode key))) - ,@(map (lambda (uid) `(li ,uid)) - values))) - ht))) - - ;; Calendar data for all events in current interval, - ;; rendered as xcal. - (div (@ (style "display:none !important;") - (id "xcal-data")) - ,((@ (vcomponent formats xcal output) ns-wrap) - (map (@ (vcomponent formats xcal output) vcomponent->sxcal) - (append regular repeating))))))))) + ,(let ((regular (fixed-events-in-range global-event-object start-date end-date)) + (repeating (repeating-events-in-range global-event-object start-date end-date))) + + `(div (@ (style "display:none !important")) + ,(calendar-event-mapping (append regular repeating)) + + ;; Calendar data for all events in current interval, + ;; rendered as xcal. + (div (@ (id "xcal-data")) + ,((@ (vcomponent formats xcal output) ns-wrap) + (map vcomponent->sxcal regular))) + + (div (@ (id "xcal-data-repeating")) + ,((@ (vcomponent formats xcal output) ns-wrap) + (map vcomponent->sxcal + ;; TODO possibly create generate-reccurrence-set-in-interval + (map (lambda (event) + (delete-parameter! (prop* event 'DTSTART) '-X-HNH-ORIGINAL) + (delete-parameter! (prop* event 'DTEND) '-X-HNH-ORIGINAL) + event) + (stream->list + (events-between + start-date end-date + (interleave-streams + ev-timestring (as-datetime (prop ev 'DTSTART)))) + (vcomponent->sxcal ev)) + repeating))))) diff --git a/module/calp/html/view/calendar/month.scm b/module/calp/html/view/calendar/month.scm index 1c162aaa..5678db0f 100644 --- a/module/calp/html/view/calendar/month.scm +++ b/module/calp/html/view/calendar/month.scm @@ -12,9 +12,10 @@ :select (really-long-event? events-between)) :use-module ((calp html vcomponent) - :select (make-block output-uid)) + :select (make-block)) :use-module ((vcomponent util group) :select (group-stream get-groups-between)) + :use-module ((vcomponent recurrence) :select (repeating?)) ) ;; (stream event-group) -> sxml @@ -84,7 +85,9 @@ (events-between pre-start post-end events)) `(popup-element (@ (class "vevent") - (data-uid ,(output-uid event))))) + (data-uid ,(prop event 'UID)) + ,@(when (repeating? event) + `((data-instance ,(datetime->string (as-datetime (prop event 'DTSTART))))))))) (template (@ (id "vevent-block")) diff --git a/module/calp/html/view/calendar/week.scm b/module/calp/html/view/calendar/week.scm index 78abcfbf..1303f134 100644 --- a/module/calp/html/view/calendar/week.scm +++ b/module/calp/html/view/calendar/week.scm @@ -15,13 +15,14 @@ event-zero-length? events-between)) :use-module ((calp html vcomponent) - :select (make-block output-uid) ) + :select (make-block) ) ;; :use-module ((calp html components) ;; :select ()) :use-module (calp translation) :use-module ((vcomponent util group) :select (group-stream get-groups-between)) :use-module (ice-9 format) + :use-module ((vcomponent recurrence) :select (repeating?)) ) @@ -62,7 +63,9 @@ (events-between start-date end-date events)) `(popup-element (@ (class "vevent") - (data-uid ,(output-uid event))))))) + ,@(when (repeating? event) + `((data-instance ,(datetime->string (as-datetime (prop event 'DTSTART)))))) + (data-uid ,(prop event 'UID))))))) ;; This template is here, instead of in (calp html calendar) since it only diff --git a/module/calp/server/routes.scm b/module/calp/server/routes.scm index 3d90cc04..37111c71 100644 --- a/module/calp/server/routes.scm +++ b/module/calp/server/routes.scm @@ -32,6 +32,12 @@ :autoload (vcomponent util instance) (global-event-object) + :use-module (vcomponent recurrence) + :use-module ((vcomponent recurrence internal) :select (until)) + :use-module (srfi srfi-41) + :use-module (srfi srfi-41 util) + :use-module (hnh util uuid) + :use-module (calp util config) :use-module (calp html view calendar) :use-module ((calp html view search) :select (search-result-page)) @@ -207,7 +213,7 @@ ;; TODO this fails when dtstart is . ;; @var{cal} should be the name of the calendar encoded in base64. - (POST "/insert" (cal data) + (POST "/insert" (cal data submit_type) (unless (and cal data) (return (build-response code: 400) @@ -269,8 +275,65 @@ (parameterize ((warnings-are-errors #t)) (catch* - (lambda () (add-and-save-event global-event-object + (lambda () + (if (repeating? event) + (cond + ((not (string? submit_type)) + (scm-error 'wrong-type-arg #f + "Expected string, got ~s" (list submit_type) #f) + ) + ((string=? submit_type "this") + ;; Change this instance only + ;; set EXDATE + (string->datetime (prop event 'X-HNH-INSTANCE-ID)) + ) + ;; change this and future instances + ((string=? submit_type "this_future") + ;; mark original event's RRULE with until this event + ;; create new event, which should be really similar + (let* ((og-event (get-event-by-uid global-event-object (prop event 'UID))) + (occurences (generate-recurrence-set og-event)) + (pairs (stream-zip occurences (stream-cdr occurences))) + (dt (string->datetime (prop event 'X-HNH-INSTANCE-ID)))) + ;; find recurrence instance before this one + (let ((last-of-old-instances + (car + (stream-find (lambda (p) (datetime= dt (cadr (prop p 'DTSTART)))) + pairs)))) + (let ((event-a event) + (event-b (copy-vcomponent event))) + (set! + (prop event-a 'RRULE) + (set-> (prop og-event 'RRULE) + (until (prop last-of-old-instances 'DTSTART))) + (prop event-a 'DTSTART) (prop og-event 'DTSTART) + (prop event-a 'DTEND) (prop og-event 'DTEND) + (prop event-b 'UID) (uuid) + ) + ) + )) + ;; use its date as UNTIL in RRULE + ) + ((string=? submit_type "all") + ;; change all instances + (let ((og-event (get-event-by-uid global-event-object (prop event 'UID)))) + (set! + (prop event 'DTSTART) + (datetime date: (as-date (prop og-event 'DTSTART)) + time: (as-time (prop event 'DTSTART))) + (prop event 'DTEND) + (datetime date: (as-date (prop og-event 'DTEND)) + time: (as-time (prop event 'DTEND)))) + (add-and-save-event global-event-object + calendar event))) + (else + (scm-error 'misc-error #f + "Invalid submit_type: ~s" + (list submit_type) #f))) + ;; else (if not repeating) + (add-and-save-event global-event-object calendar event)) + ) (warning (lambda (err fmt args) (define str (format #f "~?" fmt args)) diff --git a/module/vcomponent/util/instance/methods.scm b/module/vcomponent/util/instance/methods.scm index 7a1d2fc8..6bafb274 100644 --- a/module/vcomponent/util/instance/methods.scm +++ b/module/vcomponent/util/instance/methods.scm @@ -10,10 +10,12 @@ ;; :use-module (vcomponent parse) :use-module ((vcomponent util parse-cal-path) :select (parse-cal-path)) :use-module ((vcomponent recurrence) :select (generate-recurrence-set repeating?)) - :use-module ((vcomponent datetime) :select (ev-time calendar-files: calendar-files)) + (define-method (get-event-by-uid (this ) uid) (hash-ref (slot-ref this 'uid-map) uid)) @@ -70,10 +74,24 @@ (define-method (fixed-events-in-range (this ) start end) + ;; TODO why not use events-between here? (filter-sorted (lambda (ev) ((in-date-range? start end) (as-date (prop ev 'DTSTART)))) (slot-ref this 'fixed-events))) +(define-method (repeating-events-in-range (this ) start end) + (filter (lambda (ev) + (cond ((date/-time< end (prop ev 'DTSTART)) #f) + ((and=> (prop ev 'RRULE) rr:until) => (lambda (u) (date/-time< u start))) + ((prop ev 'RRULE) + => (lambda (rr) + (or (not (or (rr:until rr) (rr:count rr))) + ;; else if recurrence set overlaps interval + ;; TODO check this with tests/annoying-events.scm + (stream-find (lambda (instance) (event-overlaps? instance start end)) + (generate-recurrence-set ev))))))) + (slot-ref this 'repeating-events))) + (define-method (initialize (this ) args) (next-method) @@ -85,6 +103,7 @@ (let ((calendars (load-calendars (slot-ref this 'calendar-files)))) (apply add-calendars this calendars))) + (define-method (add-calendars (this ) . calendars) (slot-set! this 'calendars (append calendars (slot-ref this 'calendars))) @@ -166,61 +185,66 @@ (prop event 'UID))) (slot-ref this 'event-set))) - (hash-set! (slot-ref this 'uid-map) (prop event 'UID) - #f)) + (remove-child! (parent event) event) + (hash-remove! (slot-ref this 'uid-map) (prop event 'UID))) -(define-method (add-and-save-event (this ) calendar event) - (cond - [(get-event-by-uid this (prop event 'UID)) - => (lambda (old-event) +(define-method (update-and-save-event (this ) calendar old-event) + ;; remove old instance of event from runtime + (remove-event this old-event) - ;; remove old instance of event from runtime - (remove-event this old-event) - (remove-child! (parent old-event) old-event) + ;; Add new event to runtime, + ;; MUST be done after since the two events SHOULD share UID. + ;; NOTE that this can emit warnings + (add-event this calendar old-event) + (remove-child! (parent old-event) old-event) - ;; Add new event to runtime, - ;; MUST be done after since the two events SHOULD share UID. - ;; NOTE that this can emit warnings - (add-event this calendar event) + (set! (prop old-event 'LAST-MODIFIED) + (current-datetime)) - (set! (prop event 'LAST-MODIFIED) - (current-datetime)) + ;; NOTE Posibly defer save to a later point. + ;; That would allow better asyncronous preformance. - ;; NOTE Posibly defer save to a later point. - ;; That would allow better asyncronous preformance. + ;; save-event sets -X-HNH-FILENAME from the UID. This is fine + ;; since the two events are guaranteed to have the same UID. + (unless ((@ (vcomponent formats vdir save-delete) save-event) old-event) + (throw 'misc-error (_ "Saving event to disk failed."))) - ;; save-event sets -X-HNH-FILENAME from the UID. This is fine - ;; since the two events are guaranteed to have the same UID. - (unless ((@ (vcomponent formats vdir save-delete) save-event) event) - (throw 'misc-error (_ "Saving event to disk failed."))) + (unless (eq? calendar (parent old-event)) + ;; change to a new calendar + (format (current-error-port) + (_ "Unlinking old event from ~a~%") + (prop old-event '-X-HNH-FILENAME)) + ;; NOTE that this may fail, leading to a duplicate event being + ;; created (since we save beforehand). This is just a minor problem + ;; which either a better atomic model, or a propper error + ;; recovery log would solve. + ((@ (vcomponent formats vdir save-delete) remove-event) old-event)) - (unless (eq? calendar (parent old-event)) - ;; change to a new calendar - (format (current-error-port) - (_ "Unlinking old event from ~a~%") - (prop old-event '-X-HNH-FILENAME)) - ;; NOTE that this may fail, leading to a duplicate event being - ;; created (since we save beforehand). This is just a minor problem - ;; which either a better atomic model, or a propper error - ;; recovery log would solve. - ((@ (vcomponent formats vdir save-delete) remove-event) old-event)) + (format (current-error-port) + (_ "Event updated ~a~%") (prop old-event 'UID))) - (format (current-error-port) - (_ "Event updated ~a~%") (prop event 'UID)))] - [else - (add-event this calendar event) +;; Add a new event, which isn't already in the event object +(define-method (add-new-event (this ) calendar event) + (add-event this calendar event) - (set! (prop event 'LAST-MODIFIED) (current-datetime)) + (set! (prop event 'LAST-MODIFIED) (current-datetime)) - ;; NOTE Posibly defer save to a later point. - ;; That would allow better asyncronous preformance. - (unless ((@ (vcomponent formats vdir save-delete) save-event) event) - (throw 'misc-error (_ "Saving event to disk failed."))) + ;; NOTE Posibly defer save to a later point. + ;; That would allow better asyncronous preformance. + (unless ((@ (vcomponent formats vdir save-delete) save-event) event) + (throw 'misc-error (_ "Saving event to disk failed."))) - (format (current-error-port) - (_ "Event inserted ~a~%") (prop event 'UID))])) + (format (current-error-port) + (_ "Event inserted ~a~%") (prop event 'UID)) + ) + +(define-method (add-and-save-event (this ) calendar event) + (cond + [(get-event-by-uid this (prop event 'UID)) + => (lambda (old-event) (update-and-save-event this calendar old-event))] + [else (add-new-event this calendar event)])) diff --git a/static/components/edit-rrule.ts b/static/components/edit-rrule.ts index a361bdee..8575d9bc 100644 --- a/static/components/edit-rrule.ts +++ b/static/components/edit-rrule.ts @@ -11,8 +11,8 @@ import { RecurrenceRule } from '../vevent' */ class EditRRule extends ComponentVEvent { - constructor(uid?: string) { - super(uid); + constructor(uid?: string, instance?: string) { + super(uid, instance); if (!this.template) { throw 'vevent-edit-rrule template required'; @@ -25,7 +25,7 @@ class EditRRule extends ComponentVEvent { for (let el of this.querySelectorAll('[name]')) { el.addEventListener('input', () => { // console.log(this); - let data = vcal_objects.get(this.uid)!; + let data = this.getData()!; let rrule = data.getProperty('rrule') if (!rrule) { console.warn('RRUle missing from object'); @@ -41,9 +41,9 @@ class EditRRule extends ComponentVEvent { } } - connectedCallback() { - this.redraw(vcal_objects.get(this.uid)!) - } + // connectedCallback() { + // this.redraw(this.getData()!) + // } redraw(data: VEvent) { diff --git a/static/components/popup-element.ts b/static/components/popup-element.ts index 3300f885..5dc627d0 100644 --- a/static/components/popup-element.ts +++ b/static/components/popup-element.ts @@ -15,12 +15,12 @@ class PopupElement extends ComponentVEvent { * the top, and allowing global keyboard bindings to affect it. */ static activePopup: PopupElement | null = null; - constructor(uid?: string) { - super(uid); + constructor(uid?: string, instance?: string) { + super(uid, instance); /* TODO populate remaining (??) */ - let obj = vcal_objects.get(this.uid); + let obj = this.getData(); if (obj && obj.calendar) { this.dataset.calendar = obj.calendar; } @@ -48,8 +48,6 @@ class PopupElement extends ComponentVEvent { let template = document.getElementById('popup-template') as HTMLTemplateElement let body = (template.content.cloneNode(true) as DocumentFragment).firstElementChild!; - let uid = this.uid; - /* nav bar */ let nav = body.getElementsByClassName("popup-control")[0] as HTMLElement; bind_popup_control(nav); @@ -61,7 +59,8 @@ class PopupElement extends ComponentVEvent { maximize_btn.addEventListener('click', () => this.maximize()); let remove_btn = body.querySelector('.popup-control .remove-button') as HTMLButtonElement - remove_btn.addEventListener('click', () => remove_event(uid)); + /* TODO repeating removal */ + remove_btn.addEventListener('click', () => remove_event(this.uid)); /* end nav bar */ this.replaceChildren(body); @@ -147,7 +146,7 @@ class PopupElement extends ComponentVEvent { */ function setup_popup_element(ev: VEvent): PopupElement { let uid = ev.getProperty('uid'); - let popup = new PopupElement(uid); + let popup = new PopupElement(uid, ev.getProperty('dtstart')?.format('~Y-~m-~dT~H:~M:~S')); ev.register(popup); /* TODO propper way to find popup container */ (document.querySelector('.days') as Element).appendChild(popup); diff --git a/static/components/tab-group-element.ts b/static/components/tab-group-element.ts index e90997e9..11949870 100644 --- a/static/components/tab-group-element.ts +++ b/static/components/tab-group-element.ts @@ -26,8 +26,8 @@ class TabGroupElement extends ComponentVEvent { tabs: HTMLElement[] = []; tabLabels: HTMLElement[] = []; - constructor(uid?: string) { - super(uid); + constructor(uid?: string, instance?: string) { + super(uid!, instance!); this.menu = makeElement('div', {}, { role: 'tablist', @@ -43,7 +43,8 @@ class TabGroupElement extends ComponentVEvent { if (!this.has_rrule_tab()) { /* Note that EditRRule register itself to be updated on changes to the event */ - this.addTab(new EditRRule(data.getProperty('uid')), + this.addTab(new EditRRule(data.getProperty('uid'), + data.getProperty('dtstart')?.format('~Y-~m-~dT~H:~M:~S')), "↺", "Upprepningar"); } } else { @@ -71,7 +72,7 @@ class TabGroupElement extends ComponentVEvent { } /* redraw might add or remove tabs depending on our data, so call it here */ - this.redraw(vcal_objects.get(this.uid)!); + this.redraw(this.getData()!); /* All tabs should now be ready, focus the first one */ if (this.tabLabels.length > 0) { @@ -144,7 +145,7 @@ class TabGroupElement extends ComponentVEvent { if (tab.firstChild) { let child = tab.firstChild as HTMLElement; if (isRedrawable(child)) { - vcal_objects.get(this.uid)?.unregister(child) + this.getData()?.unregister(child) } } diff --git a/static/components/vevent-block.ts b/static/components/vevent-block.ts index 8cf61d30..f97a0e54 100644 --- a/static/components/vevent-block.ts +++ b/static/components/vevent-block.ts @@ -10,8 +10,8 @@ import { parseDate, to_local } from '../lib' A grahpical block in the week view. */ class ComponentBlock extends ComponentVEvent { - constructor(uid?: string) { - super(uid); + constructor(uid?: string, instance?: string) { + super(uid!, instance!); if (!this.template) { throw 'vevent-block template required'; diff --git a/static/components/vevent-description.ts b/static/components/vevent-description.ts index f0d224be..efa619c4 100644 --- a/static/components/vevent-description.ts +++ b/static/components/vevent-description.ts @@ -9,8 +9,8 @@ import { formatters } from '../formatters' */ class ComponentDescription extends ComponentVEvent { - constructor(uid?: string) { - super(uid); + constructor(uid?: string, instance?: string) { + super(uid!, instance!); if (!this.template) { throw 'vevent-description template required'; } diff --git a/static/components/vevent-edit.ts b/static/components/vevent-edit.ts index bf72678c..108146a3 100644 --- a/static/components/vevent-edit.ts +++ b/static/components/vevent-edit.ts @@ -14,8 +14,8 @@ import { to_boolean, gensym } from '../lib' */ class ComponentEdit extends ComponentVEvent { - constructor(uid?: string) { - super(uid); + constructor(uid?: string, instance?: string) { + super(uid, instance); if (!this.template) { throw 'vevent-edit template required'; @@ -39,10 +39,10 @@ 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.get(this.uid) + let data = this.getData() if (!data) { - throw `Data missing for uid ${this.dataset.uid}.` + throw `Data missing for uid ${this.getKey()}.` } @@ -59,7 +59,7 @@ class ComponentEdit extends ComponentVEvent { el.addEventListener('change', (e) => { let v = (e.target as HTMLSelectElement).selectedOptions[0].value - let obj = vcal_objects.get(this.uid)! + let obj = this.getData()! obj.calendar = v; }); } @@ -70,7 +70,7 @@ class ComponentEdit extends ComponentVEvent { for (let el of this.querySelectorAll("[data-property]")) { // console.log(el); el.addEventListener('input', (e) => { - let obj = vcal_objects.get(this.uid) + let obj = this.getData() // console.log(el, e); if (obj === undefined) { throw 'No object with uid ' + this.uid @@ -117,18 +117,25 @@ class ComponentEdit extends ComponentVEvent { /* TODO unselecting and reselecting this checkbox deletes all entered data. Cache it somewhere */ if (has_repeats.checked) { - vcal_objects.get(this.uid)!.setProperty('rrule', new RecurrenceRule()) + this.getData()!.setProperty('rrule', new RecurrenceRule()) } else { /* TODO is this a good way to remove a property ? */ - vcal_objects.get(this.uid)!.setProperty('rrule', undefined) + this.getData()!.setProperty('rrule', undefined) } }) } let submit = this.querySelector('form') as HTMLFormElement + /* TODO If start or end DATE is changed, only allow THIS */ + /* if only time component was changed, allow all */ submit.addEventListener('submit', (e) => { console.log(submit, e); - create_event(vcal_objects.get(this.uid)!); + // submit button pressed (e.submitter); + let submit_type = 'all'; + if (e.submitter) { + submit_type = e.submitter.dataset.key!; + } + create_event(submit_type, this.getData()!); e.preventDefault(); return false; diff --git a/static/components/vevent.ts b/static/components/vevent.ts index 5852a2ff..abf0f4c0 100644 --- a/static/components/vevent.ts +++ b/static/components/vevent.ts @@ -12,12 +12,13 @@ abstract class ComponentVEvent extends HTMLElement { template: HTMLTemplateElement | null uid: string + instance: string | null - constructor(uid?: string) { + constructor(uid?: string, instance?: string) { super(); this.template = document.getElementById(this.tagName.toLowerCase()) as HTMLTemplateElement | null - let real_uid; + let real_uid, real_instance; if (uid) { // console.log('Got UID directly'); @@ -29,6 +30,7 @@ abstract class ComponentVEvent extends HTMLElement { // console.log('Had UID as direct attribute'); real_uid = this.dataset.uid; } else { + /* TODO when is this case relevant? */ let el = this.closest('[data-uid]') if (el) { // console.log('Found UID higher up in the tree'); @@ -48,7 +50,32 @@ abstract class ComponentVEvent extends HTMLElement { this.uid = real_uid; this.dataset.uid = real_uid; - vcal_objects.get(this.uid)?.register(this); + if (instance) { + real_instance = instance + } else { + if (this.dataset.instance) { + real_instance = this.dataset.instance + } else { + let el = this.closest('[data-instance]') + if (el) { + real_instance = (el as HTMLElement).dataset.instance + if (real_instance === undefined) { + real_instance = null; + } + } else { + real_instance = null + } + } + } + this.instance = real_instance; + if (real_instance) { + this.dataset.instance = real_instance; + } + + /* TODO */ + /* Here, choose different rules depending on if we are repeating or not */ + + vcal_objects.get(this.getKey())?.register(this); /* We DON'T have a redraw here in the general case, since the HTML rendered server-side should be fine enough for us. @@ -56,12 +83,27 @@ abstract class ComponentVEvent extends HTMLElement { should take care of that some other way */ } - connectedCallback() { + getKey(): string { let uid = this.dataset.uid - if (uid) { - let v = vcal_objects.get(uid) - if (v) this.redraw(v); - } + // let instance_id = ev.getProperty('dtstart')!.format('~Y-~m-~dT~H:~M:~S') + let instance = this.dataset.instance + if (!uid) throw new Error("UID missing"); + let key = uid; + /* NOTE proper composite keys would be nice, since this can collide with + a regular key. However, javascript's Map doesn't support lists (or + object) as keys by default. */ + if (instance) key += '---' + instance; + return key; + } + + /* This return different object for different instances */ + getData(): VEvent | undefined { + return vcal_objects.get(this.getKey()) + } + + connectedCallback() { + let v = this.getData(); + if (v) this.redraw(v); } abstract redraw(data: VEvent): void diff --git a/static/event-creator.ts b/static/event-creator.ts index 0f2c42b4..6ce04d78 100644 --- a/static/event-creator.ts +++ b/static/event-creator.ts @@ -69,7 +69,7 @@ class EventCreator { that.ev.calendar = window.default_calendar; // let ev_block = document.createElement('vevent-block') as ComponentBlock; - let ev_block = new ComponentBlock(that.ev.getProperty('uid')); + let ev_block = new ComponentBlock(that.ev.getProperty('uid'), that.ev.getProperty('dtstart')?.format('~Y-~m-~dT~H:~M:~S')); ev_block.classList.add('generated'); that.event = ev_block; that.ev.register(ev_block); diff --git a/static/script.ts b/static/script.ts index ec771773..79365e7d 100644 --- a/static/script.ts +++ b/static/script.ts @@ -20,12 +20,29 @@ window.addEventListener('load', function() { */ // let json_objects_el = document.getElementById('json-objects'); - let div = document.getElementById('xcal-data')!; - let vevents = div.firstElementChild!.children; - for (let vevent of vevents) { - let ev = xml_to_vcal(vevent); - vcal_objects.set(ev.getProperty('uid'), ev) + {/* Load "regular" (non-repeating) events */ + let div = document.getElementById('xcal-data')!; + let vevents = div.firstElementChild!.children; + + for (let vevent of vevents) { + let ev = xml_to_vcal(vevent); + vcal_objects.set(ev.getProperty('uid'), ev) + } + } + + {/* Load repeating events */ + let div = document.getElementById('xcal-data-repeating')!; + let vevents = div.firstElementChild!.children; + for (let vevent of vevents) { + let ev = xml_to_vcal(vevent); + /* NOTE manuall calculation of key, since getKey is method of vevent + component, not the abstract vevent */ + let instance_id = ev.getProperty('dtstart')! + .format('~Y-~m-~dT~H:~M:~S') + let key = ev.getProperty('uid') + '---' + instance_id + vcal_objects.set(key, ev); + } } @@ -38,6 +55,7 @@ window.addEventListener('load', function() { throw "UID required" } event_calendar_mapping.set(uid, calendar_name); + /* TODO this fails for repeating events */ let obj = vcal_objects.get(uid); if (obj) obj.calendar = calendar_name } diff --git a/static/server_connect.ts b/static/server_connect.ts index d1a544eb..d2617bf9 100644 --- a/static/server_connect.ts +++ b/static/server_connect.ts @@ -55,7 +55,7 @@ async function remove_event(uid: uid) { // ]; // } -async function create_event(event: VEvent) { +async function create_event(submit_type: string, event: VEvent) { // let xml = event.getElementsByTagName("icalendar")[0].outerHTML let calendar = event.calendar; @@ -67,6 +67,9 @@ async function create_event(event: VEvent) { console.log('calendar=', calendar/*, xml*/); let data = new URLSearchParams(); + + data.append('submit_type', submit_type); + data.append("cal", calendar); // data.append("data", xml); diff --git a/static/vevent.ts b/static/vevent.ts index 6a2c6f0f..a7f67a7f 100644 --- a/static/vevent.ts +++ b/static/vevent.ts @@ -555,3 +555,32 @@ function xml_to_vcal(xml: Element): VEvent { return new VEvent(property_map, component_list) } + + +class MultiInstanceVEvent extends VEvent { + + private base_event: VEvent | null = null; + private overlayProperties: Map; + + constructor(parent: VEvent) { + super(); + + this.base_event = parent; + + this.overlayProperties = new Map + + /* Add this to alternative set of parent */ + } + + setProperty(key: string, value: any, type?: ical_type) { + /* TODO type resolution a setPropertyInternal */ + this.overlayProperties.set(key.toUpperCase(), value); + } + + getProperty(key: string): any | any[] | undefined { + let a = this.overlayProperties.get(key.toUpperCase()) + if (a !== undefined) return a + return super.getProperty(key) + } + +} -- cgit v1.2.3