From 1484155c211fe8452344ffdc501e858706ecbc51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Wed, 29 Sep 2021 23:36:21 +0200 Subject: Start rework on js setup. --- module/calp/html/vcomponent.scm | 335 ++++++++++++------------ module/calp/html/view/calendar.scm | 64 +++-- module/calp/html/view/calendar/week.scm | 93 ++++++- module/vcomponent/vdir/save-delete.scm | 2 +- static/globals.js | 257 +++++++++++++++++++ static/script.js | 437 ++++++++++++++++---------------- static/vcal.js | 2 + 7 files changed, 781 insertions(+), 409 deletions(-) create mode 100644 static/globals.js diff --git a/module/calp/html/vcomponent.scm b/module/calp/html/vcomponent.scm index 105c6cc5..4b3e9ec7 100644 --- a/module/calp/html/vcomponent.scm +++ b/module/calp/html/vcomponent.scm @@ -62,54 +62,56 @@ optional: (attributes '()) key: (fmt-header list)) ;; (format (current-error-port) "fmt-single-event: ~a~%" (prop ev 'X-HNH-FILENAME)) - `(div (@ ,@(assq-merge - attributes - `((data-bindby "bind_view") - (class " eventtext summary-tab " - ,(when (and (prop ev 'PARTSTAT) - (eq? 'TENTATIVE (prop ev 'PARTSTAT))) - " tentative "))))) - (h3 ,(fmt-header - (when (prop ev 'RRULE) - `(span (@ (class "repeating")) "↺")) - `(span (@ (class "bind summary") - (data-property "summary")) - ,(prop ev 'SUMMARY)))) - (div - ,(call-with-values (lambda () (fmt-time-span ev)) - (case-lambda [(start) - `(div (time (@ (class "bind dtstart") - (data-property "dtstart") - (data-fmt ,(string-append "~L" start)) - (datetime ,(datetime->string - (as-datetime (prop ev 'DTSTART)) - "~1T~3"))) - ,(datetime->string - (as-datetime (prop ev 'DTSTART)) - start)))] - [(start end) - `(div (time (@ (class "bind dtstart") - (data-property "dtstart") - (data-fmt ,(string-append "~L" start)) - (datetime ,(datetime->string - (as-datetime (prop ev 'DTSTART)) - "~1T~3"))) - ,(datetime->string (as-datetime (prop ev 'DTSTART)) - start)) - " — " - (time (@ (class "bind dtend") - (data-property "dtend") - (data-fmt ,(string-append "~L" end)) - (datetime ,(datetime->string - (as-datetime (prop ev 'DTSTART)) - "~1T~3"))) - ,(datetime->string (as-datetime (prop ev 'DTEND)) - end)))])) - - ;; TODO add optional fields when added in frontend - ;; Possibly by always having them here, just hidden. - - (div (@ (class "fields")) + `(vevent-description + (@ ,@(assq-merge + attributes + `( + (class " vevent eventtext summary-tab " + ,(when (and (prop ev 'PARTSTAT) + (eq? 'TENTATIVE (prop ev 'PARTSTAT))) + " tentative ")) + (data-uid ,(prop ev 'UID))))) + (h3 ,(fmt-header + (when (prop ev 'RRULE) + `(span (@ (class "repeating")) "↺")) + `(span (@ (class "bind summary") + (data-property "summary")) + ,(prop ev 'SUMMARY)))) + (div + ,(call-with-values (lambda () (fmt-time-span ev)) + (case-lambda [(start) + `(div (time (@ (class "bind dtstart") + (data-property "dtstart") + (data-fmt ,(string-append "~L" start)) + (datetime ,(datetime->string + (as-datetime (prop ev 'DTSTART)) + "~1T~3"))) + ,(datetime->string + (as-datetime (prop ev 'DTSTART)) + start)))] + [(start end) + `(div (time (@ (class "bind dtstart") + (data-property "dtstart") + (data-fmt ,(string-append "~L" start)) + (datetime ,(datetime->string + (as-datetime (prop ev 'DTSTART)) + "~1T~3"))) + ,(datetime->string (as-datetime (prop ev 'DTSTART)) + start)) + " — " + (time (@ (class "bind dtend") + (data-property "dtend") + (data-fmt ,(string-append "~L" end)) + (datetime ,(datetime->string + (as-datetime (prop ev 'DTSTART)) + "~1T~3"))) + ,(datetime->string (as-datetime (prop ev 'DTEND)) + end)))])) + + ;; TODO add optional fields when added in frontend + ;; Possibly by always having them here, just hidden. + + (div (@ (class "fields")) ,(when (and=> (prop ev 'LOCATION) (negate string-null?)) `(div (b "Plats: ") (div (@ (class "bind location") (data-property "location")) @@ -118,7 +120,7 @@ ,(awhen (prop ev 'DESCRIPTION) `(div (@ (class "bind description") (data-property "description")) - ,(format-description ev it))) + ,(format-description ev it))) ,@(awhen (prop* ev 'ATTACH) ;; attach satisfies @code{vline?} @@ -193,123 +195,127 @@ `(div (@ (class "last-modified")) "Senast ändrad " ,(datetime->string (prop ev 'LAST-MODIFIED) "~1 ~H:~M")))) - ))) + ))) (define*-public (fmt-for-edit ev optional: (attributes '()) key: (fmt-header list)) - `(div (@ (class " eventtext edit-tab ") - (data-bindby "bind_edit")) - (form (@ (class "edit-form")) - (div (@ (class "dropdown-goes-here"))) - (h3 (input (@ (type "text") - (placeholder "Sammanfattning") - (name "summary") (required) - (class "bind") (data-property "summary") - (value ,(prop ev 'SUMMARY))))) - - ,(let ((start (prop ev 'DTSTART)) - (end (prop ev 'DTEND))) - `(div (@ (class "timeinput")) - - ,@(with-label - "Starttid" - `(div (@ (class "date-time bind") - (data-bindby "bind_date_time") - (name "dtstart")) - (input (@ (type "date") - (value ,(date->string (as-date start))))) - (input (@ (type "time") - (value ,(time->string (as-time start) "~H:~M")) - ,@(when (date? start) '((disabled))) - )))) - - ;; TODO some way to add an endtime if missing beforehand - ;; TODO, actually proper support for event without end times - ,@(when end - (with-label - "Sluttid" - `(div (@ (class "date-time bind") - (data-bindby "bind_date_time") - (name "dtend")) - (input (@ (type "date") - (value ,(date->string (as-date end))))) - (input (@ (type "time") - (value ,(time->string (as-time end) "~H:~M")) - ,@(when (date? end) '((disabled)))))))) - - (div - ,@(with-label - "Heldag?" - `(input (@ (type "checkbox") - (class "bind") - (data-bindby "bind_wholeday") - (name "wholeday") - ,@(when (date? start) '((checked))))))) - - )) - - ,@(with-label - "Plats" - `(input (@ (placeholder "Plats") - (name "location") - (type "text") - (class "bind") (data-property "location") - (value ,(or (prop ev 'LOCATION) ""))))) - - ,@(with-label - "Beskrivning" - `(textarea (@ (placeholder "Beskrivning") - (class "bind") (data-property "description") - (name "description")) - ,(prop ev 'DESCRIPTION))) - - ,@(with-label - "Kategorier" - ;; It would be better if these input-list's worked on the same - ;; class=bind system as the fields above. The problem with that - ;; is however that each input-list requires different search - ;; and join procedures. Currently this is bound in the JS, see - ;; [CATEGORIES_BIND]. - ;; It matches on ".input-list[data-property='categories']". - `(div (@ (class "input-list") - (data-property "categories")) - ,@(awhen (prop ev 'CATEGORIES) - (map (lambda (c) - `(input (@ (size 2) - (class "unit") - (value ,c)))) - it)) - - (input (@ (class "unit final") - (size 2) - (type "text") + `(vevent-edit (@ (class "vevent") + (data-uid ,(prop ev 'UID))))) + +(define-public (edit-template) + `(div (@ (class " eventtext edit-tab ")) + (form (@ (class "edit-form")) + (div (@ (class "dropdown-goes-here"))) + (h3 (input (@ (type "text") + (placeholder "Sammanfattning") + (name "summary") (required) + (class "bind") (data-property "summary") + ; (value ,(prop ev 'SUMMARY)) + ))) + + (div (@ (class "timeinput")) + + ,@(with-label + "Starttid" + `(div (@ (class "date-time") + (name "dtstart")) + (input (@ (type "date") + ; (value ,(date->string (as-date start))) + )) + (input (@ (type "time") + ; (value ,(time->string (as-time start) "~H:~M")) + ; ,@(when (date? start) '((disabled))) + )))) + + ;; TODO some way to add an endtime if missing beforehand + ;; TODO, actually proper support for event without end times + ,@(with-label + "Sluttid" + `(div (@ (class "date-time") + (name "dtend")) + (input (@ (type "date") + ; (value ,(date->string (as-date end))) + )) + (input (@ (type "time") + ; (value ,(time->string (as-time end) "~H:~M")) + ; ,@(when (date? end) '((disabled))) + )))) + + (div + ,@(with-label + "Heldag?" + `(input (@ (type "checkbox") + (name "wholeday") + ; ,@(when (date? start) '((checked))) )))) - (hr) - - ;; For custom user fields - ;; TODO these are currently not bound to anything, so entering data - ;; here does nothing. Bigest hurdle to overcome is supporting arbitrary - ;; fields which will come and go in the JavaScript. - ;; TODO also, all (most? maybe not LAST-MODIFIED) remaining properties - ;; should be exposed here. - (div (@ (class "input-list")) - (div (@ (class "unit final newfield")) - (input (@ (type "text") - (list "known-fields") - (placeholder "Nytt fält"))) - (select (@ (name "TYPE")) - (option (@ (value "TEXT")) "Text")) - (span - (input (@ (type "text") - (placeholder "Värde")))))) - - (hr) - - - (input (@ (type "submit"))) - ))) + ) + + ,@(with-label + "Plats" + `(input (@ (placeholder "Plats") + (name "location") + (type "text") + (data-property "location") + ; (value ,(or (prop ev 'LOCATION) "")) + ))) + + ,@(with-label + "Beskrivning" + `(textarea (@ (placeholder "Beskrivning") + (data-property "description") + (name "description")) + ; ,(prop ev 'DESCRIPTION) + )) + + ,@(with-label + "Kategorier" + ;; It would be better if these input-list's worked on the same + ;; class=bind system as the fields above. The problem with that + ;; is however that each input-list requires different search + ;; and join procedures. Currently this is bound in the JS, see + ;; [CATEGORIES_BIND]. + ;; It matches on ".input-list[data-property='categories']". + `(div (@ (class "input-list") + (data-property "categories")) + #; + ,@(awhen (prop ev 'CATEGORIES) + (map (lambda (c) + `(input (@ (size 2) + (class "unit") + (value ,c)))) + it)) + + (input (@ (class "unit final") + (size 2) + (type "text") + )))) + + (hr) + + ;; For custom user fields + ;; TODO these are currently not bound to anything, so entering data + ;; here does nothing. Bigest hurdle to overcome is supporting arbitrary + ;; fields which will come and go in the JavaScript. + ;; TODO also, all (most? maybe not LAST-MODIFIED) remaining properties + ;; should be exposed here. + (div (@ (class "input-list")) + (div (@ (class "unit final newfield")) + (input (@ (type "text") + (list "known-fields") + (placeholder "Nytt fält"))) + (select (@ (name "TYPE")) + (option (@ (value "TEXT")) "Text")) + (span + (input (@ (type "text") + (placeholder "Värde")))))) + + (hr) + + + (input (@ (type "submit"))) + ))) ;; Single event in side bar (text objects) @@ -357,12 +363,12 @@ `((a (@ (href "#" ,(html-id ev)) (class "hidelink")) - (div (@ ,@(assq-merge + (vevent-block (@ ,@(assq-merge extra-attributes `((id ,(html-id ev)) (data-calendar ,(html-attr (or (prop (parent ev) 'NAME) "unknown"))) ;; (data-bindon "bind_view") - (class "event CAL_" ,(html-attr (or (prop (parent ev) 'NAME) + (class "vevent event CAL_" ,(html-attr (or (prop (parent ev) 'NAME) "unknown")) ,(when (and (prop ev 'PARTSTAT) (eq? 'TENTATIVE (prop ev 'PARTSTAT))) @@ -371,6 +377,7 @@ (eq? 'TRANSPARENT (prop ev 'TRANSP))) " transparent") ) + (data-uid ,(prop ev 'UID)) (onclick "toggle_popup('popup' + this.id)") ))) ;; Inner div to prevent overflow. Previously "overflow: none" @@ -391,10 +398,12 @@ ,(when (and=> (prop ev 'DESCRIPTION) (negate string-null?)) `(span (@ (class "description")) "🗎"))) + #; (div (@ (style "display:none !important;")) ,((@ (vcomponent xcal output) ns-wrap) ((@ (vcomponent xcal output) vcomponent->sxcal) - ev))))))) + ev))) + )))) (define (repeat-info event) diff --git a/module/calp/html/view/calendar.scm b/module/calp/html/view/calendar.scm index 4574f517..00451984 100644 --- a/module/calp/html/view/calendar.scm +++ b/module/calp/html/view/calendar.scm @@ -111,12 +111,16 @@ (script (@ (defer) (src "/static/clock.js"))) (script (@ (defer) (src "/static/popup.js"))) (script (@ (defer) (src "/static/rrule.js"))) - (script (@ (defer) (src "/static/binders.js"))) + ;; (script (@ (defer) (src "/static/binders.js"))) (script (@ (defer) (src "/static/server_connect.js"))) - (script (@ (defer) (src "/static/input_list.js"))) + ;; (script (@ (defer) (src "/static/input_list.js"))) (script (@ (defer) (src "/static/date_time.js"))) - (script (@ (defer) (src "/static/vcal.js"))) + ;; (script (@ (defer) (src "/static/vcal.js"))) (script (@ (defer) (src "/static/script.js"))) + (script (@ (defer) (src "/static/globals.js"))) + + ;; on load + ,(calendar-styles calendars) ,@(when (debug) @@ -251,15 +255,15 @@ `(li (@ (class "CAL_" ,(html-attr (prop calendar 'NAME)))) (a (@ (href "/search?" - ,((@ (web uri-query) encode-query-parameters) - `((q . (and (date/-time<=? - ,(current-datetime) - (prop event 'DTSTART)) - ;; TODO this seems to miss some calendars, - ;; I belive it's due to some setting X-WR-CALNAME, - ;; which is only transfered /sometimes/ into NAME. - (string=? ,(->string (prop calendar 'NAME)) - (or (prop (parent event) 'NAME) "")))))))) + ,((@ (web uri-query) encode-query-parameters) + `((q . (and (date/-time<=? + ,(current-datetime) + (prop event 'DTSTART)) + ;; TODO this seems to miss some calendars, + ;; I belive it's due to some setting X-WR-CALNAME, + ;; which is only transfered /sometimes/ into NAME. + (string=? ,(->string (prop calendar 'NAME)) + (or (prop (parent event) 'NAME) "")))))))) ,(prop calendar 'NAME)))) calendars)) (div (@ (id "calendar-dropdown-template") (class "template")) @@ -311,17 +315,19 @@ ;; it. description: "")))) (event (car (children cal)))) - `((div (@ (class "template event-container") (id "event-template") - ;; Only needed to create a duration. So actual dates - ;; dosen't matter - (data-start "2020-01-01") - (data-end "2020-01-02")) - ,(caddar ; strip tag - (make-block event `((class " generated "))))) + `( + ;; (div (@ (class "template event-container") (id "event-template") + ;; ;; Only needed to create a duration. So actual dates + ;; ;; dosen't matter + ;; (data-start "2020-01-01") + ;; (data-end "2020-01-02")) + ;; ,(caddar ; strip tag + ;; (make-block event `((class " generated "))))) ;; TODO merge this into the event-set, add attribute ;; for non-displaying elements. - (div (@ (class "template") (id "popup-template")) - ,(popup event (string-append "popup" (html-id event)))))) + ;; (div (@ (class "template") (id "popup-template")) + ;; ,(popup event (string-append "popup" (html-id event)))) + )) ;; Auto-complets when adding new fields to a component ;; Any string is however still valid. @@ -344,4 +350,18 @@ RDATE RRULE ACTION REPEAT TRIGGER CREATED DTSTAMP LAST-MODIFIED SEQUENCE REQUEST-STATUS - )))))) + ))) + + (div (@ (style "display:none !important;") + (id "xcal-data")) + ,((@ (vcomponent xcal output) ns-wrap) + (map (@ (vcomponent xcal output) vcomponent->sxcal) + (stream->list + (filter-sorted-stream + (lambda (ev) + ((@ (vcomponent datetime) event-overlaps?) + ev start-date + (date+ end-date (date day: 1)))) + events)))))) + + )) diff --git a/module/calp/html/view/calendar/week.scm b/module/calp/html/view/calendar/week.scm index 556c3d85..340db7d5 100644 --- a/module/calp/html/view/calendar/week.scm +++ b/module/calp/html/view/calendar/week.scm @@ -52,10 +52,95 @@ ,@(for event in (stream->list (events-between start-date end-date events)) - ((@ (calp html vcomponent ) popup) event (string-append "popup" (html-id event)))) - - ))))) - + ((@ (calp html vcomponent ) popup) + event (string-append "popup" (html-id event)))) + + )) + ;; description in sidebar / tab of popup + (template (@ (id "vevent-description")) + ,(description-template) + ) + + ;; edit tab of popup + (template (@ (id "vevent-edit")) + ,((@ (calp html vcomponent) edit-template))) + + ;; "physical" block + (template (@ (id "vevent-block")) + ,(block-template) + ) + + ))) + +;; based on the output of fmt-single-event +(define (description-template) + '(div (@ (class " eventtext summary-tab " ())) + (h3 ((span (@ (class "repeating")) "↺") + (span (@ (class "bind summary") + (data-property "summary")) + "Test"))) + (div (div (time (@ (class "bind dtstart") + (data-property "dtstart") + (data-fmt "~L~H:~M") + (datetime "2021-09-29T19:56:46")) + "19:56") + "\xa0—\xa0" + (time (@ (class "bind dtend") + (data-property "dtend") + (data-fmt "~L~H:~M") + (datetime "2021-09-29T19:56:46")) + "20:56")) + (div (@ (class "fields")) + (div (b "Plats: ") + (div (@ (class "bind location") + (data-property "location")) + "Alsättersgatan 13")) + (div (@ (class "bind description") + (data-property "description")) + ("With a description")) + (div (@ (class "categories")) + (a (@ (class "category") + (href "/search/?" + "q=%28member%20%22test%22%20%28or%20%28prop%20event%20%28quote%20CATEGORIES%29%29%20%28quote%20%28%29%29%29%29")) + test)) + (div (@ (class "rrule")) + "Upprepas " + "varje vecka" + ".") + (div (@ (class "last-modified")) + "Senast ändrad " + "2021-09-29 19:56"))))) + +(define (block-template) + `(div (@ ; (id ,(html-id ev)) + (data-calendar "unknown") + (class "event CAL_unknown" + ;; ,(when (and (prop ev 'PARTSTAT) + ;; (eq? 'TENTATIVE (prop ev 'PARTSTAT))) + ;; " tentative") + ;; ,(when (and (prop ev 'TRANSP) + ;; (eq? 'TRANSPARENT (prop ev 'TRANSP))) + ;; " transparent") + ) + (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 + ;; overflow (for the tabs?). + (div (@ (class "event-body")) + `(span (@ (class "repeating")) ; "↺" + ) + (span (@ (class "bind summary") + (data-property "summary")) + ; ,(format-summary ev (prop ev 'SUMMARY)) + ) + `(span (@ (class "bind location") + (data-property "location"))) + ;; Document symbol when we have text + `(span (@ (class "description")) + ; "🗎" + )) + ) ) (define (time-marker-div) diff --git a/module/vcomponent/vdir/save-delete.scm b/module/vcomponent/vdir/save-delete.scm index d17b595e..b3c7f9c5 100644 --- a/module/vcomponent/vdir/save-delete.scm +++ b/module/vcomponent/vdir/save-delete.scm @@ -11,7 +11,7 @@ (define-module (vcomponent vdir save-delete) :use-module (calp util) - :use-module ((calp util exceptions) :select (assert)) + :use-module ((calp util exceptions) :select (assert)) :use-module (vcomponent ical output) :use-module (vcomponent) :use-module ((calp util io) :select (with-atomic-output-to-file)) diff --git a/static/globals.js b/static/globals.js new file mode 100644 index 00000000..b881c531 --- /dev/null +++ b/static/globals.js @@ -0,0 +1,257 @@ +"use strict"; + +class VEventValue { + constructor (type, value, parameters = {}) { + this.type = type; + this.value = value; + this.parameters = parameters; + } +} + +/* maybe ... */ +class VEventDuration extends VEventValue { +} + +class VEvent { + constructor (properties = {}, components = []) { + this.properties = properties; + this.components = components; + this.registered = []; + } + + getProperty (key) { + let e = this.properties[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); + } else { + e.value = value; + } + for (let el of this.registered) { + el.redraw(this); + } + } + + register (htmlNode) { + this.registered.push(htmlNode); + } +} + +function make_vevent_value (value_tag) { + /* TODO parameters */ + return new VEventValue (value_tag.tagName, make_vevent_value_ (value_tag)); +} + +function make_vevent_value_ (value_tag) { + /* 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 'recur': + /* TODO parse */ + return ""; + + case 'uc-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; + } +} + + +/* xml dom object -> class VEvent */ +function xml_to_vcal (xml) { + /* 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 = {} + if (properties) { + for (var i = 0; i < properties.childElementCount; i++) { + let tag = properties.childNodes[i]; + let parameters = {}; + let value = []; + for (var j = 0; j < tag.childElementCount; j++) { + let child = tag.childNodes[j]; + switch (tag.tagName) { + case 'parameters': + parameters = /* 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); + } + } + property_map[tag.tagName] = value; + } + } + + let component_list = [] + if (components) { + for (let child of components.childNodes) { + component_list.push(xml_to_vcal(child)) + } + } + + return new VEvent(property_map, component_list) +} + +const vcal_objects = {}; + +class ComponentVEvent extends HTMLElement { + constructor () { + super (); + this.template = document.getElementById(this.tagName); + + /* We DON'T have a redraw here in the general case, since the + HTML rendered server-side should be fine enough for us. + Those that need a direct rerendering (such as the edit tabs) + should take care of that some other way */ + } + + redraw (data) { + // update ourselves from template + + if (! this.template) { + throw "Something"; + } + + let body = this.template.content.cloneNode(true).firstElementChild; + + for (let el of body.getElementsByClassName("bind")) { + let p = el.dataset.property; + let d; + if ((d = data.getProperty(p))) { + /* TODO format */ + el.innerHTML = d; + } + } + + this.replaceChildren(body); + } + +} + +class ComponentDescription extends ComponentVEvent { + constructor () { + super() ; + } + +} + +class ComponentEdit extends ComponentVEvent { + constructor () { + super(); + + /* Edit tab is rendered here. It's left blank server-side, since + it only makes sense to have something here if we have javascript */ + this.redraw(vcal_objects[this.dataset.uid]); + } +} + +class ComponentBlock extends ComponentVEvent { + constructor () { + super(); + } +} + +window.addEventListener('load', function () { + + // let json_objects_el = document.getElementById('json-objects'); + let div = document.getElementById('xcal-data'); + let vevents = div.firstElementChild.childNodes; + + for (let vevent of vevents) { + let ev = xml_to_vcal(vevent); + vcal_objects[ev.getProperty('uid')] = ev + } + + /* + - .popup + - .block + - .list + */ + let vevent_els = document.getElementsByClassName('vevent') + for (let el of vevent_els) { + try { + vcal_objects[el.dataset.uid].register(el); + } catch { + console.error("Invalid something, uid = ", el.dataset.uid, + "el = ", el + ); + } + } + + customElements.define('vevent-description', ComponentDescription); + customElements.define('vevent-edit', ComponentEdit); + customElements.define('vevent-block', ComponentBlock); +}) + diff --git a/static/script.js b/static/script.js index a0d58c27..05ae61c5 100644 --- a/static/script.js +++ b/static/script.js @@ -4,211 +4,211 @@ calp specific stuff */ -class EventCreator { - - /* dynamicly created event when dragging */ - constructor() { - this.event = false; - this.event_start = { x: NaN, y: NaN }; - this.down_on_event = false; - } - - create_empty_event () { - /* TODO this doesn't clone JS attributes */ - - let event = document.getElementById("event-template") - .firstChild.cloneNode(true); - let popup = document.getElementById("popup-template") - .firstChild.cloneNode(true); - - /* -------------------- */ - /* Manually transfer or recreate attributes which we still need */ - /* TODO shouldn't these use transferListeners (or similar)? - See input_list.js:transferListeners */ - - for (let dt of popup.getElementsByClassName("date-time")) { - init_date_time_single(dt); - } - - popup.getElementsByClassName("edit-form")[0].onsubmit = function () { - create_event(event); - return false; /* stop default */ - } - - /* -------------------- */ - /* Fix tabs for newly created popup */ - - let id = gensym ("__js_event"); - - // TODO remove button? - // $("button 2??").onclick = `remove_event(document.getElementById('${id}'))` - - let tabgroup_id = gensym(); - for (let tab of popup.querySelectorAll(".tabgroup .tab")) { - let new_id = gensym(); - let input = tab.querySelector("input"); - input.id = new_id; - input.name = tabgroup_id; - tab.querySelector("label").setAttribute('for', new_id); - } - - let nav = popup.getElementsByClassName("popup-control")[0]; - bind_popup_control(nav); - - /* -------------------- */ - - // TODO download links - - /* -------------------- */ - - event.id = id; - popup.id = "popup" + id; - - return [popup, event]; - } - - create_event_down (intended_target) { - let that = this; - return function (e) { - /* Only trigger event creation stuff on actuall events background, - NOT on its children */ - that.down_on_event = false; - if (e.target != intended_target) return; - that.down_on_event = true; - - that.event_start.x = e.clientX; - that.event_start.y = e.clientY; - } - } - - /* - round_to: what start and end times should round to when dragging, in fractions - of the width of the containing container. - - TODO limit this to only continue when on the intended event_container. - - (event → [0, 1)), 𝐑, bool → event → () - */ - create_event_move(pos_in, round_to=1, wide_element=false) { - let that = this; - return function (e) { - if (e.buttons != 1 || ! that.down_on_event) return; - - /* Create event when we start moving the mouse. */ - 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; } - - /* only allow start of dragging on background */ - if (e.target != this) return; - - /* only on left click */ - if (e.buttons != 1) return; - - let [popup, event] = that.create_empty_event(); - that.event = event; - - /* TODO better solution to add popup to DOM */ - document.getElementsByTagName("main")[0].append(popup); - - /* [0, 1) -- where are we in the container */ - /* Ronud to force steps of quarters */ - /* NOTE for in-day events a floor here work better, while for - all day events I want a round, but which has the tip over point - around 0.7 instead of 0.5. - It might also be an idea to subtract a tiny bit from the short events - mouse position, since I feel I always get to late starts. - */ - let time = round_time(pos_in(this, e), round_to); - - event.dataset.time1 = time; - event.dataset.time2 = time; - - /* ---------------------------------------- */ - - this.appendChild(event); - - /* requires that event is child of an '.event-container'. */ - new VComponent( - event, - wide_element=wide_element); - // bind_properties(event, wide_element); - - /* requires that dtstart and dtend properties are initialized */ - - /* ---------------------------------------- */ - - /* Makes all current events transparent when dragging over them. - Without this weird stuff happens when moving over them - - This includes ourselves. - */ - for (let e of this.children) { - e.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 event_container = that.event.closest(".event-container"); - - /* These two are in UTC */ - let container_start = parseDate(event_container.dataset.start); - let container_end = parseDate(event_container.dataset.end); - - /* ---------------------------------------- */ - - /* ms */ - let duration = container_end - container_start; - - 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 - timezone doesn't give me) - */ - /* TODO Should these inherit UTC from container_*? */ - let d1 = new Date(container_start.getTime() + start_in_duration) - let d2 = new Date(container_start.getTime() + end_in_duration) - - // console.log(that.event); - console.log(d1, d2); - that.event.properties.dtstart = d1; - that.event.properties.dtend = d2; - } - } - - create_event_finisher (callback) { - let that = this; - return function create_event_up (e) { - 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 = ""; - } - - place_in_edit_mode(that.event); - - let localevent = that.event; - that.event = null; - - callback (localevent); - - } - } -} +// class EventCreator { +// +// /* dynamicly created event when dragging */ +// constructor() { +// this.event = false; +// this.event_start = { x: NaN, y: NaN }; +// this.down_on_event = false; +// } +// +// create_empty_event () { +// /* TODO this doesn't clone JS attributes */ +// +// let event = document.getElementById("event-template") +// .firstChild.cloneNode(true); +// let popup = document.getElementById("popup-template") +// .firstChild.cloneNode(true); +// +// /* -------------------- */ +// /* Manually transfer or recreate attributes which we still need */ +// /* TODO shouldn't these use transferListeners (or similar)? +// See input_list.js:transferListeners */ +// +// for (let dt of popup.getElementsByClassName("date-time")) { +// init_date_time_single(dt); +// } +// +// popup.getElementsByClassName("edit-form")[0].onsubmit = function () { +// create_event(event); +// return false; /* stop default */ +// } +// +// /* -------------------- */ +// /* Fix tabs for newly created popup */ +// +// let id = gensym ("__js_event"); +// +// // TODO remove button? +// // $("button 2??").onclick = `remove_event(document.getElementById('${id}'))` +// +// let tabgroup_id = gensym(); +// for (let tab of popup.querySelectorAll(".tabgroup .tab")) { +// let new_id = gensym(); +// let input = tab.querySelector("input"); +// input.id = new_id; +// input.name = tabgroup_id; +// tab.querySelector("label").setAttribute('for', new_id); +// } +// +// let nav = popup.getElementsByClassName("popup-control")[0]; +// bind_popup_control(nav); +// +// /* -------------------- */ +// +// // TODO download links +// +// /* -------------------- */ +// +// event.id = id; +// popup.id = "popup" + id; +// +// return [popup, event]; +// } +// +// create_event_down (intended_target) { +// let that = this; +// return function (e) { +// /* Only trigger event creation stuff on actuall events background, +// NOT on its children */ +// that.down_on_event = false; +// if (e.target != intended_target) return; +// that.down_on_event = true; +// +// that.event_start.x = e.clientX; +// that.event_start.y = e.clientY; +// } +// } +// +// /* +// round_to: what start and end times should round to when dragging, in fractions +// of the width of the containing container. +// +// TODO limit this to only continue when on the intended event_container. +// +// (event → [0, 1)), 𝐑, bool → event → () +// */ +// create_event_move(pos_in, round_to=1, wide_element=false) { +// let that = this; +// return function (e) { +// if (e.buttons != 1 || ! that.down_on_event) return; +// +// /* Create event when we start moving the mouse. */ +// 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; } +// +// /* only allow start of dragging on background */ +// if (e.target != this) return; +// +// /* only on left click */ +// if (e.buttons != 1) return; +// +// let [popup, event] = that.create_empty_event(); +// that.event = event; +// +// /* TODO better solution to add popup to DOM */ +// document.getElementsByTagName("main")[0].append(popup); +// +// /* [0, 1) -- where are we in the container */ +// /* Ronud to force steps of quarters */ +// /* NOTE for in-day events a floor here work better, while for +// all day events I want a round, but which has the tip over point +// around 0.7 instead of 0.5. +// It might also be an idea to subtract a tiny bit from the short events +// mouse position, since I feel I always get to late starts. +// */ +// let time = round_time(pos_in(this, e), round_to); +// +// event.dataset.time1 = time; +// event.dataset.time2 = time; +// +// /* ---------------------------------------- */ +// +// this.appendChild(event); +// +// /* requires that event is child of an '.event-container'. */ +// new VComponent( +// event, +// wide_element=wide_element); +// // bind_properties(event, wide_element); +// +// /* requires that dtstart and dtend properties are initialized */ +// +// /* ---------------------------------------- */ +// +// /* Makes all current events transparent when dragging over them. +// Without this weird stuff happens when moving over them +// +// This includes ourselves. +// */ +// for (let e of this.children) { +// e.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 event_container = that.event.closest(".event-container"); +// +// /* These two are in UTC */ +// let container_start = parseDate(event_container.dataset.start); +// let container_end = parseDate(event_container.dataset.end); +// +// /* ---------------------------------------- */ +// +// /* ms */ +// let duration = container_end - container_start; +// +// 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 +// timezone doesn't give me) +// */ +// /* TODO Should these inherit UTC from container_*? */ +// let d1 = new Date(container_start.getTime() + start_in_duration) +// let d2 = new Date(container_start.getTime() + end_in_duration) +// +// // console.log(that.event); +// console.log(d1, d2); +// that.event.properties.dtstart = d1; +// that.event.properties.dtend = d2; +// } +// } +// +// create_event_finisher (callback) { +// let that = this; +// return function create_event_up (e) { +// 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 = ""; +// } +// +// place_in_edit_mode(that.event); +// +// let localevent = that.event; +// that.event = null; +// +// callback (localevent); +// +// } +// } +// } @@ -254,7 +254,7 @@ function place_in_edit_mode (event) { -window.onload = function () { +window.addEventListener('load', function () { // let start_time = document.querySelector("meta[name='start-time']").content; // let end_time = document.querySelector("meta[name='end-time']").content; @@ -280,7 +280,7 @@ window.onload = function () { init_date_time(); /* Is event creation active? */ - if (EDIT_MODE) { + if (false && EDIT_MODE) { let eventCreator = new EventCreator; for (let c of document.getElementsByClassName("events")) { c.onmousedown = eventCreator.create_event_down(c); @@ -336,17 +336,17 @@ window.onload = function () { el.parentElement.removeAttribute("href"); let popup = document.getElementById("popup" + el.id); - popup.getElementsByClassName("edit-form")[0].onsubmit = function () { - create_event(el); - return false; /* stop default */ - } + // popup.getElementsByClassName("edit-form")[0].onsubmit = function () { + // create_event(el); + // return false; /* stop default */ + // } /* Bind all vcomponent properties into javascript. */ - if (el.closest(".longevents")) { - new VComponent(el, true); - } else { - new VComponent(el, false); - } + // if (el.closest(".longevents")) { + // new VComponent(el, true); + // } else { + // new VComponent(el, false); + // } } @@ -402,7 +402,7 @@ window.onload = function () { // init_arbitary_kv(); - init_input_list(); + // init_input_list(); document.addEventListener('keydown', function (event) { @@ -413,5 +413,4 @@ window.onload = function () { event.preventDefault(); } }); -} - +}) diff --git a/static/vcal.js b/static/vcal.js index 93cfc028..f86c7f65 100644 --- a/static/vcal.js +++ b/static/vcal.js @@ -9,6 +9,8 @@ cdr a procedure which takes a slot and a value and binds the value to the slot. */ + +/* DEPRECATED */ class VComponent { constructor(el, wide_event=false) { -- cgit v1.2.3