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 From e28b54810bb42b21a069a1257cf5e59e06c735a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Thu, 30 Sep 2021 01:47:38 +0200 Subject: Replace today-button with web component. --- module/calp/html/view/calendar.scm | 16 +++++++++------- static/clock.js | 38 +++++++++++++++++++++++++++++++------- static/script.js | 9 +-------- 3 files changed, 41 insertions(+), 22 deletions(-) diff --git a/module/calp/html/view/calendar.scm b/module/calp/html/view/calendar.scm index 00451984..53e928f6 100644 --- a/module/calp/html/view/calendar.scm +++ b/module/calp/html/view/calendar.scm @@ -150,6 +150,7 @@ (footer (@ (style "grid-area: footer")) (span "Page generated " ,(date->string (current-date))) + (span "Current time " (current-time (@ (interval 1)))) (span (a (@ (href ,(repo-url))) "Source Code"))) @@ -166,13 +167,14 @@ ,(btn href: (date->string (set (day start-date) 1) "/month/~1.html") "månadsvy") - ,(btn id: "today-button" - href: (string-append - "/today?" (case intervaltype - [(month) "view=month"] - [(week) "view=week"] - [else ""])) - "idag")) + (today-button + (a (@ (class "btn") + (href ,(string-append + "/today?" (case intervaltype + [(month) "view=month"] + [(week) "view=week"] + [else ""])))) + (div "idag")))) (div (@ (id "jump-to")) ;; Firefox's accessability complain about each date diff --git a/static/clock.js b/static/clock.js index 9642ebaf..240041a9 100644 --- a/static/clock.js +++ b/static/clock.js @@ -60,15 +60,39 @@ class SmallcalCellHighlight extends Clock { } } -/* Update [today] button */ -class ButtonUpdater extends Clock { - constructor(el, proc) { +/* -------------------------------------------------- */ + +class ClockElement extends HTMLElement { + constructor () { super(); - this.el = el; - this.proc = proc; } - update(now) { - this.proc(this.el, now); + connectedCallback () { + let interval = this.hasAttribute('interval') ? +this.getAttribute('img') : 60; + interval *= 1000 /* ms */ + + this.timer_id = window.setInterval(() => this.update(new Date), interval) + this.update(new Date) + } + + static get observerAttributes () { + return ['timer_id'] + } + + update (now) { /* noop */ } +} + +class TodayButton extends ClockElement { + update (now) { + this.querySelector('a').href = now.format("~Y-~m-~d.html") + } +} +customElements.define('today-button', TodayButton) + + +class CurrentTime extends ClockElement { + update (now) { + this.innerHTML = now.format('~H:~M:~S') } } +customElements.define('current-time', CurrentTime) diff --git a/static/script.js b/static/script.js index 05ae61c5..580ee520 100644 --- a/static/script.js +++ b/static/script.js @@ -82,7 +82,7 @@ // } // // /* -// round_to: what start and end times should round to when dragging, in fractions +// round_to: what start and end times should round to when dragging, in fractionsb // of the width of the containing container. // // TODO limit this to only continue when on the intended event_container. @@ -258,11 +258,6 @@ 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 button_updater = new ButtonUpdater( - document.getElementById("today-button"), - (e, d) => e.href = d.format('~Y-~m-~d') + ".html" - ); - const sch = new SmallcalCellHighlight( document.querySelector('.small-calendar')) @@ -273,7 +268,6 @@ window.addEventListener('load', function () { window.setInterval(() => { let d = new Date; timebar.update(d); - button_updater.update(d); sch.update(d); }, 1000 * 60); @@ -372,7 +366,6 @@ window.addEventListener('load', function () { href: target_href, innerHTML: gotodatebtn.innerHTML, }); - document.getElementById("today-button").href = target_href; gotodatebtn.replaceWith(golink); document.querySelector("#jump-to input[name='date']").onchange = function () { -- cgit v1.2.3 From 927bd58d3340965328f0ccd05f865175b49a6ed5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Fri, 1 Oct 2021 03:34:06 +0200 Subject: Got date-times working in new system. --- module/calp/html/vcomponent.scm | 121 +++++++++++++++----------------- module/calp/html/view/calendar/week.scm | 49 +++++++------ static/clock.js | 2 +- static/globals.js | 96 ++++++++++++++++++++++++- 4 files changed, 177 insertions(+), 91 deletions(-) diff --git a/module/calp/html/vcomponent.scm b/module/calp/html/vcomponent.scm index 4b3e9ec7..63b3df3b 100644 --- a/module/calp/html/vcomponent.scm +++ b/module/calp/html/vcomponent.scm @@ -206,11 +206,12 @@ (define-public (edit-template) `(div (@ (class " eventtext edit-tab ")) (form (@ (class "edit-form")) + ;; TODO actually have calendar list here, since we are just a template (div (@ (class "dropdown-goes-here"))) (h3 (input (@ (type "text") (placeholder "Sammanfattning") (name "summary") (required) - (class "bind") (data-property "summary") + (class "interactive") (data-property "summary") ; (value ,(prop ev 'SUMMARY)) ))) @@ -218,36 +219,22 @@ ,@(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 + '(date-time-input (@ (name "dtstart") + (class "interactive") + (data-property "dtstart") + ))) + ,@(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))) - )))) + '(date-time-input (@ (name "dtend") + (class "interactive") + (data-property "dtend")))) (div ,@(with-label "Heldag?" `(input (@ (type "checkbox") (name "wholeday") - ; ,@(when (date? start) '((checked))) )))) ) @@ -257,6 +244,7 @@ `(input (@ (placeholder "Plats") (name "location") (type "text") + (class "interactive") (data-property "location") ; (value ,(or (prop ev 'LOCATION) "")) ))) @@ -264,54 +252,55 @@ ,@(with-label "Beskrivning" `(textarea (@ (placeholder "Beskrivning") + (class "interactive") (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) + ;; ,@(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"))) diff --git a/module/calp/html/view/calendar/week.scm b/module/calp/html/view/calendar/week.scm index 340db7d5..c2165a8e 100644 --- a/module/calp/html/view/calendar/week.scm +++ b/module/calp/html/view/calendar/week.scm @@ -75,46 +75,53 @@ ;; based on the output of fmt-single-event (define (description-template) '(div (@ (class " eventtext summary-tab " ())) - (h3 ((span (@ (class "repeating")) "↺") + (h3 ((span (@ (class "repeating")) ; "↺" + ) (span (@ (class "bind summary") - (data-property "summary")) - "Test"))) + (data-property "summary"))))) (div (div (time (@ (class "bind dtstart") (data-property "dtstart") (data-fmt "~L~H:~M") - (datetime "2021-09-29T19:56:46")) - "19:56") + (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")) + (datetime ; "2021-09-29T19:56:46" + )) + ; "20:56" + )) (div (@ (class "fields")) (div (b "Plats: ") (div (@ (class "bind location") (data-property "location")) - "Alsättersgatan 13")) + ; "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" - ".") + ; "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"))))) + "Senast ändrad -" + ; "2021-09-29 19:56" + ))))) (define (block-template) `(div (@ ; (id ,(html-id ev)) (data-calendar "unknown") - (class "event CAL_unknown" + (class "event CAL_unknown" ;; ,(when (and (prop ev 'PARTSTAT) ;; (eq? 'TENTATIVE (prop ev 'PARTSTAT))) ;; " tentative") diff --git a/static/clock.js b/static/clock.js index 240041a9..d33d603a 100644 --- a/static/clock.js +++ b/static/clock.js @@ -75,7 +75,7 @@ class ClockElement extends HTMLElement { this.update(new Date) } - static get observerAttributes () { + static get observedAttributes () { return ['timer_id'] } diff --git a/static/globals.js b/static/globals.js index b881c531..4ee3c62a 100644 --- a/static/globals.js +++ b/static/globals.js @@ -35,6 +35,7 @@ class VEvent { e.value = value; } for (let el of this.registered) { + /* TODO update correct fields, allow component to redraw themselves */ el.redraw(this); } } @@ -188,10 +189,13 @@ class ComponentVEvent extends HTMLElement { for (let el of body.getElementsByClassName("bind")) { let p = el.dataset.property; - let d; + let d, fmt; if ((d = data.getProperty(p))) { - /* TODO format */ - el.innerHTML = d; + if ((fmt = el.dataset.fmt)) { + el.innerHTML = d.format(fmt); + } else { + el.innerHTML = d; + } } } @@ -211,10 +215,61 @@ class ComponentEdit extends ComponentVEvent { constructor () { super(); + this.firstTime = true; + } + + connectedCallback() { + /* 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]); + + for (let el of this.getElementsByClassName("interactive")) { + el.addEventListener('input', () => { + vcal_objects[this.dataset.uid].setProperty( + el.dataset.property, + el.value) + }); + } } + + redraw (data) { + // update ourselves from template + + if (! this.template) { + throw "Something"; + } + + let body; + if (this.firstTime) { + body = this.template.content.cloneNode(true).firstElementChild; + } else { + body = this; + } + + for (let el of body.getElementsByClassName("interactive")) { + let p = el.dataset.property; + let d; + if ((d = data.getProperty(p))) { + /* + https://stackoverflow.com/questions/57157830/how-can-i-specify-the-sequence-of-running-nested-web-components-constructors + */ + 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; + }); + } + } + + if (this.firstTime) { + this.replaceChildren(body); + this.firstTime = false; + } + } + } class ComponentBlock extends ComponentVEvent { @@ -255,3 +310,38 @@ window.addEventListener('load', function () { customElements.define('vevent-block', ComponentBlock); }) + + +class DateTimeInput extends HTMLElement { + constructor () { + super(); + this.innerHTML = '' + } + + get value () { + let date = this.querySelector("[type='date']").value; + let time = this.querySelector("[type='time']").value; + return parseDate(date + 'T' + time) + } + + set value (new_value) { + let date, time; + if (new_value instanceof Date) { + date = new_value.format("~L~Y-~m-~d"); + time = new_value.format("~L~H:~M:~S"); + } else { + [date, time] = new_value.split('T') + } + this.querySelector("[type='date']").value = date; + this.querySelector("[type='time']").value = time; + } + + addEventListener(type, proc) { + if (type != 'input') throw "Only input supported"; + + this.querySelector("[type='date']").addEventListener(type, proc); + this.querySelector("[type='time']").addEventListener(type, proc); + } +} + +customElements.define('date-time-input', DateTimeInput) -- cgit v1.2.3 From 81adf54a8a36beba9622c1929937c871a751b2d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Fri, 1 Oct 2021 03:37:31 +0200 Subject: Remove old date_time system. --- module/calp/html/view/calendar.scm | 2 +- static/binders.js | 34 ---------------------------------- static/date_time.js | 36 ------------------------------------ static/script.js | 2 -- 4 files changed, 1 insertion(+), 73 deletions(-) delete mode 100644 static/date_time.js diff --git a/module/calp/html/view/calendar.scm b/module/calp/html/view/calendar.scm index 53e928f6..f8188352 100644 --- a/module/calp/html/view/calendar.scm +++ b/module/calp/html/view/calendar.scm @@ -114,7 +114,7 @@ ;; (script (@ (defer) (src "/static/binders.js"))) (script (@ (defer) (src "/static/server_connect.js"))) ;; (script (@ (defer) (src "/static/input_list.js"))) - (script (@ (defer) (src "/static/date_time.js"))) + ;; (script (@ (defer) (src "/static/date_time.js"))) ;; (script (@ (defer) (src "/static/vcal.js"))) (script (@ (defer) (src "/static/script.js"))) (script (@ (defer) (src "/static/globals.js"))) diff --git a/static/binders.js b/static/binders.js index a6e37189..ac83b284 100644 --- a/static/binders.js +++ b/static/binders.js @@ -114,37 +114,3 @@ function bind_wholeday(el, e) { } }); } - - -/* used for dtstart and dtend input boxes - init_date_time MUST be called beforehand -*/ -function bind_date_time(el, e) { - e.addEventListener('input', function () { - let dt = el.properties[e.name].value; - if (e.value == '') return; - let y, m, d, h, s; - switch (this.type) { - case 'date': - [y,m,d] = this.value.split('-') - dt.setYear(Number(y)/* - 1900 */); - dt.setMonth(Number(m) - 1); - dt.setDate(d); - break; - case 'time': - [h,m,s] = this.value.split(':') - dt.setHours(Number(h)); - dt.setMinutes(Number(m)); - dt.setSeconds(0); - break; - default: - console.log("How did you get here??? ", e); - } - - el.properties[e.name] = dt; - }); - - el.properties.get_callback_list(e.name).push( - [e, (s, v) => s.value = v.format("~Y-~m-~dT~H:~M")]); - -} diff --git a/static/date_time.js b/static/date_time.js deleted file mode 100644 index 8b7249dd..00000000 --- a/static/date_time.js +++ /dev/null @@ -1,36 +0,0 @@ -function init_date_time_single(dt) { - dt.time = dt.querySelector('[type=time]'); - dt.date = dt.querySelector('[type=date]'); - - Object.defineProperty(dt, 'value', { - get: () => (dt.date.value && dt.time.value) - // TODO wrapping tag - ? dt.date.value + "T" + dt.time.value - : "", - set: (v) => [dt.date.value, dt.time.value] = v.split("T"), - }); - - Object.defineProperty(dt, 'name', { - get: () => dt.attributes.name.value - }); - - dt._addEventListener = dt.addEventListener; - dt.addEventListener = function (field, proc) { - /* input events are propagated to children - other events target ourselves */ - switch (field) { - case 'input': - dt.time.addEventListener(field, proc); - dt.date.addEventListener(field, proc); - break; - default: - dt._addEventListener(field, proc); - } - } -} - -function init_date_time() { - for (let dt of document.getElementsByClassName("date-time")) { - init_date_time_single(dt); - } -} diff --git a/static/script.js b/static/script.js index 580ee520..b4fb7bda 100644 --- a/static/script.js +++ b/static/script.js @@ -271,8 +271,6 @@ window.addEventListener('load', function () { sch.update(d); }, 1000 * 60); - init_date_time(); - /* Is event creation active? */ if (false && EDIT_MODE) { let eventCreator = new EventCreator; -- cgit v1.2.3 From 98a56b782d1ced056c77019e88b4bcea4a270f83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Fri, 1 Oct 2021 11:49:26 +0200 Subject: Reintroduce dateonly for date-time-input:s. --- module/calp/html/vcomponent.scm | 1 + static/binders.js | 24 ------------------------ static/globals.js | 40 ++++++++++++++++++++++++++++++++++++++-- static/lib.js | 28 +++++++++++++++++++++++++++- 4 files changed, 66 insertions(+), 27 deletions(-) diff --git a/module/calp/html/vcomponent.scm b/module/calp/html/vcomponent.scm index 63b3df3b..b32bc0c4 100644 --- a/module/calp/html/vcomponent.scm +++ b/module/calp/html/vcomponent.scm @@ -235,6 +235,7 @@ "Heldag?" `(input (@ (type "checkbox") (name "wholeday") + (onclick "wholeday_checkbox(this)") )))) ) diff --git a/static/binders.js b/static/binders.js index ac83b284..a3742aec 100644 --- a/static/binders.js +++ b/static/binders.js @@ -90,27 +90,3 @@ function bind_view(el, e) { let f = (s, v) => s.innerText = v.format(s.dataset && s.dataset.fmt); el.properties.get_callback_list(e.dataset.property).push([e, f]); } - - -function bind_wholeday(el, e) { - let popup = popup_from_event(el); - let wholeday = popup.querySelector("input[name='wholeday']"); - wholeday.addEventListener('click', function (event) { - for (let f of popup.querySelectorAll("input[type='time']")) { - f.disabled = wholeday.checked; - } - - for (let f of ['dtstart', 'dtend']) { - let param = el.properties[f]; - if (! param) continue; /* dtend optional */ - let d = param.value; - if (wholeday.checked) { - param.type = 'date'; - } else { - param.type = 'date-time'; - } - d.isWholeDay = wholeday.checked; - el.properties[f] = d; - } - }); -} diff --git a/static/globals.js b/static/globals.js index 4ee3c62a..fd576e26 100644 --- a/static/globals.js +++ b/static/globals.js @@ -318,10 +318,40 @@ class DateTimeInput extends HTMLElement { this.innerHTML = '' } + static get observedAttributes () { + return [ 'dateonly' ] + } + + attributeChangedCallback (name, from, to) { + console.log(this, name, boolean(from), boolean(to)); + switch (name) { + case 'dateonly': + this.querySelector('[type="time"]').disabled = boolean(to) + break; + } + } + + get dateonly () { + return boolean(this.getAttribute('dateonly')); + } + + set dateonly (bool) { + this.setAttribute ('dateonly', bool); + } + get value () { + + let dt; let date = this.querySelector("[type='date']").value; - let time = this.querySelector("[type='time']").value; - return parseDate(date + 'T' + time) + if (boolean(this.getAttribute('dateonly'))) { + dt = parseDate(date); + dt.type = 'date'; + } else { + let time = this.querySelector("[type='time']").value; + dt = parseDate(date + 'T' + time) + dt.type = 'date-time'; + } + return dt; } set value (new_value) { @@ -345,3 +375,9 @@ class DateTimeInput extends HTMLElement { } customElements.define('date-time-input', DateTimeInput) + +function wholeday_checkbox (box) { + box.closest('.timeinput') + .getElementsByTagName('date-time-input') + .forEach(el => el.dateonly = box.checked); +} diff --git a/static/lib.js b/static/lib.js index 1d42100c..100f4161 100644 --- a/static/lib.js +++ b/static/lib.js @@ -129,6 +129,24 @@ function asList(thing) { } +function boolean (value) { + 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; + } +} + + + /* internal */ function datepad(thing, width=2) { return (thing + "").padStart(width, "0"); @@ -159,7 +177,8 @@ function format_date(date, str) { } return outstr; } -Object.prototype.format = function () { return "" + this; } /* any number of arguments */ + +Object.prototype.format = function (/* any number of arguments */) { return "" + this; } Date.prototype.format = function (str) { return format_date (this, str); } /* @@ -176,4 +195,11 @@ DOMTokenList.prototype.find = function (regexp) { } } +/* HTMLCollection is the result of a querySelectorAll */ +HTMLCollection.prototype.forEach = function (proc) { + for (let el of this) { + proc(el); + } +} + const xcal = "urn:ietf:params:xml:ns:icalendar-2.0"; -- cgit v1.2.3 From a31bfa6686eb52506d600a5b77a96fddeb9fb344 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Sun, 3 Oct 2021 00:47:50 +0200 Subject: to_jcal for new VEvent class. --- static/globals.js | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/static/globals.js b/static/globals.js index fd576e26..c414fc5b 100644 --- a/static/globals.js +++ b/static/globals.js @@ -6,6 +6,44 @@ class VEventValue { this.value = value; this.parameters = parameters; } + + to_jcal () { + 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; + } + return [this.parameters, this.type, value]; + } } /* maybe ... */ @@ -43,6 +81,16 @@ class VEvent { register (htmlNode) { this.registered.push(htmlNode); } + + to_jcal () { + let out_properties = [] + for (let [key, value] of Object.entries(this.properties)) { + let sub = value.to_jcal(); + sub.unshift(key) + out_properties.push(sub); + } + return ['vevent', out_properties, [/* alarms go here*/]] + } } function make_vevent_value (value_tag) { -- cgit v1.2.3 From 0ddb299f256a6be037a904b84c4b2cdcdffe18c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Sun, 3 Oct 2021 00:48:04 +0200 Subject: Fix graphical block size. --- static/globals.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/static/globals.js b/static/globals.js index c414fc5b..6efba94f 100644 --- a/static/globals.js +++ b/static/globals.js @@ -324,6 +324,21 @@ class ComponentBlock extends ComponentVEvent { constructor () { super(); } + + redraw (data) { + super.redraw(data); + + let p; + if ((p = data.getProperty('dtstart'))) { + this.style.top = date_to_percent(p, 1) + "%"; + console.log('dtstart', p); + } + if ((p = data.getProperty('dtend'))) { + this.style.height = 'unset'; + console.log('dtend', p); + this.style.bottom = (100 - date_to_percent(p, 1)) + "%"; + } + } } window.addEventListener('load', function () { -- cgit v1.2.3 From 3cf3e26352698f1dc4b7447563bfc1605173f9f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Sun, 3 Oct 2021 00:48:11 +0200 Subject: Repair debug buttons. --- module/calp/html/vcomponent.scm | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/module/calp/html/vcomponent.scm b/module/calp/html/vcomponent.scm index b32bc0c4..5600646a 100644 --- a/module/calp/html/vcomponent.scm +++ b/module/calp/html/vcomponent.scm @@ -616,9 +616,13 @@ "som xCal"))) ,@(when (debug) `((ul - (li (button (@ (onclick "console.log(event_to_jcal(event_from_popup(this.closest('.popup-container'))));")) "js")) - (li (button (@ (onclick "console.log(jcal_to_xcal(event_to_jcal(event_from_popup(this.closest('.popup-container')))));")) "xml")) - (li (button (@ (onclick "console.log(event_from_popup(this.closest('.popup-container')))")) "this")) + ;; this.closest('.vevent').dataset['uid'] + (li (button (@ (onclick ,(format #f "console.log(vcal_objects['~a'].to_jcal())" + (prop ev 'UID)))) "js")) + (li (button (@ (onclick ,(format #f "console.log(jcal_to_xcal(vcal_objects['~a'].to_jcal()))" + (prop ev 'UID)))) "xml")) + (li (button (@ (onclick ,(format #f "console.log(vcal_objects['~a'])" + (prop ev 'UID)))) "this")) )))) )) -- cgit v1.2.3 From a64c2a665af1abe0b91f1c5eb1f97df91ed8a4de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Sun, 3 Oct 2021 17:48:13 +0200 Subject: Further work, rework popup. --- module/calp/html/vcomponent.scm | 18 +- module/calp/html/view/calendar.scm | 1 + module/calp/html/view/calendar/week.scm | 42 +++- static/dragable.js | 6 +- static/globals.js | 309 +++++++---------------- static/popup.js | 12 +- static/script.js | 421 ++++++++++++++++---------------- static/style.scss | 30 +-- 8 files changed, 381 insertions(+), 458 deletions(-) diff --git a/module/calp/html/vcomponent.scm b/module/calp/html/vcomponent.scm index 5600646a..c5ae452d 100644 --- a/module/calp/html/vcomponent.scm +++ b/module/calp/html/vcomponent.scm @@ -1,5 +1,6 @@ (define-module (calp html vcomponent) :use-module (calp util) + :use-module ((calp util exceptions) :select (warning)) :use-module (vcomponent) :use-module (srfi srfi-1) :use-module (srfi srfi-26) @@ -66,10 +67,9 @@ (@ ,@(assq-merge attributes `( - (class " vevent eventtext summary-tab " - ,(when (and (prop ev 'PARTSTAT) - (eq? 'TENTATIVE (prop ev 'PARTSTAT))) - " tentative ")) + (class ,(when (and (prop ev 'PARTSTAT) + (eq? 'TENTATIVE (prop ev 'PARTSTAT))) + " tentative ")) (data-uid ,(prop ev 'UID))))) (h3 ,(fmt-header (when (prop ev 'RRULE) @@ -368,7 +368,8 @@ " transparent") ) (data-uid ,(prop ev 'UID)) - (onclick "toggle_popup('popup' + this.id)") + ;; TODO figure out stable way to get popup for element + ;; (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 @@ -571,10 +572,13 @@ )))) + + (define-public (popup ev id) - `(div (@ (id ,id) (class "popup-container CAL_" + (warning "popup is deprecated") + `(div (@ (id ,id) (class "popup-container CAL_" ,(html-attr (or (prop (parent ev) 'NAME) - "unknown"))) + "unknown"))) (onclick "event.stopPropagation()")) ;; TODO all (?) code uses .popup-container as the popup, while .popup sits and does nothing. ;; Do something about this? diff --git a/module/calp/html/view/calendar.scm b/module/calp/html/view/calendar.scm index f8188352..f447773d 100644 --- a/module/calp/html/view/calendar.scm +++ b/module/calp/html/view/calendar.scm @@ -117,6 +117,7 @@ ;; (script (@ (defer) (src "/static/date_time.js"))) ;; (script (@ (defer) (src "/static/vcal.js"))) (script (@ (defer) (src "/static/script.js"))) + (script (@ (defer) (src "/static/vevent.js"))) (script (@ (defer) (src "/static/globals.js"))) ;; on load diff --git a/module/calp/html/view/calendar/week.scm b/module/calp/html/view/calendar/week.scm index c2165a8e..5361ab65 100644 --- a/module/calp/html/view/calendar/week.scm +++ b/module/calp/html/view/calendar/week.scm @@ -14,6 +14,9 @@ events-between)) :use-module ((calp html vcomponent) :select (make-block) ) + :use-module ((calp html components) + :select (btn tabset #; #; form with-label + )) :use-module ((vcomponent group) :select (group-stream get-groups-between)) ) @@ -52,10 +55,16 @@ ,@(for event in (stream->list (events-between start-date end-date events)) - ((@ (calp html vcomponent ) popup) - event (string-append "popup" (html-id event)))) + `(popup-element + (@ (class "vevent") + (data-uid ,(prop event 'UID))) + ) + #; + ((@ (calp html vcomponent ) popup) ; + event (string-append "popup" (html-id event)))) )) + ;; description in sidebar / tab of popup (template (@ (id "vevent-description")) ,(description-template) @@ -70,11 +79,38 @@ ,(block-template) ) + ;; Based on popup:s output + (template + (@ (id "popup-template")) + (div (@ ; (id ,id) + (class "popup-container CAL_" + #; + ,(html-attr (or (prop (parent ev) 'NAME) ; + "unknown"))) + (onclick "event.stopPropagation()")) + (div (@ (class "popup")) + (nav (@ (class "popup-control")) + ,(btn "×" + title: "Stäng" + onclick: "" + ;; onclick: "close_popup(document.getElementById(this.closest('.popup-container').id))" + class: '("close-tooltip"))) + + ,(tabset + `(("📅" title: "Översikt" + (vevent-description (@ (class "populate-with-uid"))) + ) + + ,@(when (edit-mode) + `(("📅" title: "Redigera" + (vevent-edit (@ (class "populate-with-uid")))))))))) + ) + ))) ;; based on the output of fmt-single-event (define (description-template) - '(div (@ (class " eventtext summary-tab " ())) + '(div (@ (class " vevent eventtext summary-tab " ())) (h3 ((span (@ (class "repeating")) ; "↺" ) (span (@ (class "bind summary") diff --git a/static/dragable.js b/static/dragable.js index 41895760..6eb0b999 100644 --- a/static/dragable.js +++ b/static/dragable.js @@ -21,14 +21,16 @@ function bind_popup_control (nav) { 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-container"); + let popup = nav.closest("popup-element"); nav.dataset.startPoint = popup.offsetLeft + ";" + popup.offsetTop; } 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 popup = nav.closest(".popup-container"); + // let popup = nav.closest(".popup-container"); + let popup = nav.closest("popup-element"); popup.style.left = startX + (e.clientX - x) + "px"; popup.style.top = startY + (e.clientY - y) + "px"; diff --git a/static/globals.js b/static/globals.js index 6efba94f..41472264 100644 --- a/static/globals.js +++ b/static/globals.js @@ -1,218 +1,5 @@ "use strict"; -class VEventValue { - constructor (type, value, parameters = {}) { - this.type = type; - this.value = value; - this.parameters = parameters; - } - - to_jcal () { - 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; - } - return [this.parameters, this.type, value]; - } -} - -/* 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) { - /* TODO update correct fields, allow component to redraw themselves */ - el.redraw(this); - } - } - - register (htmlNode) { - this.registered.push(htmlNode); - } - - to_jcal () { - let out_properties = [] - for (let [key, value] of Object.entries(this.properties)) { - let sub = value.to_jcal(); - sub.unshift(key) - out_properties.push(sub); - } - return ['vevent', out_properties, [/* alarms go here*/]] - } -} - -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 { @@ -226,6 +13,13 @@ class ComponentVEvent extends HTMLElement { should take care of that some other way */ } + connectedCallback () { + let uid; + if ((uid = this.dataset.uid)) { + this.redraw (vcal_objects[uid]); + } + } + redraw (data) { // update ourselves from template @@ -270,7 +64,14 @@ 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 */ - this.redraw(vcal_objects[this.dataset.uid]); + + let data = vcal_objects[this.dataset.uid] + + if (! data) { + throw `Data missing for uid ${this.dataset.uid}.` + } + + this.redraw(data); for (let el of this.getElementsByClassName("interactive")) { el.addEventListener('input', () => { @@ -320,9 +121,31 @@ class ComponentEdit extends ComponentVEvent { } +function find_popup (uid) { + for (let el of vcal_objects[uid].registered) { + if (el.tagName === 'popup-element') { + return el; + } + } + throw 'Popup not fonud'; +} + +function find_block (uid) { + for (let el of vcal_objects[uid].registered) { + if (el.tagName === 'vevent-block') { + return el; + } + } + throw 'Popup not fonud'; +} + class ComponentBlock extends ComponentVEvent { constructor () { super(); + + this.addEventListener('click', () => { + toggle_popup(find_popup(this.dataset.uid)); + }); } redraw (data) { @@ -330,13 +153,13 @@ class ComponentBlock extends ComponentVEvent { let p; if ((p = data.getProperty('dtstart'))) { - this.style.top = date_to_percent(p, 1) + "%"; - console.log('dtstart', p); + this.style.top = date_to_percent(to_local(p), 1) + "%"; + // console.log('dtstart', p); } if ((p = data.getProperty('dtend'))) { this.style.height = 'unset'; - console.log('dtend', p); - this.style.bottom = (100 - date_to_percent(p, 1)) + "%"; + // console.log('dtend', p); + this.style.bottom = (100 - date_to_percent(to_local(p), 1)) + "%"; } } } @@ -439,6 +262,56 @@ class DateTimeInput extends HTMLElement { customElements.define('date-time-input', DateTimeInput) +class PopupElement extends HTMLElement { + constructor () { + super(); + + /* TODO populate remaining */ + // this.id = 'popup' + this.dataset.uid + } + + redraw () { + console.log('IMPLEMENT ME'); + } + + connectedCallback() { + let body = document.getElementById('popup-template').content.cloneNode(true).firstElementChild; + + let uid = this.dataset.uid + // console.log(uid); + + body.getElementsByClassName('populate-with-uid') + .forEach((e) => e.setAttribute('data-uid', uid)); + + /* tabs */ + let tabgroup_id = gensym(); + for (let tab of body.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); + } + /* end tabs */ + + /* nav bar */ + let nav = body.getElementsByClassName("popup-control")[0]; + bind_popup_control(nav); + + let btn = body.querySelector('.popup-control .close-tooltip') + btn.addEventListener('click', () => { + close_popup(this); + }); + /* end nav bar */ + + this.replaceChildren(body); + } +} + +window.addEventListener('load', function () { + customElements.define('popup-element', PopupElement) +}); + function wholeday_checkbox (box) { box.closest('.timeinput') .getElementsByTagName('date-time-input') diff --git a/static/popup.js b/static/popup.js index e19db6f2..0b04b280 100644 --- a/static/popup.js +++ b/static/popup.js @@ -2,12 +2,14 @@ /* event component => coresponding popup component */ function event_from_popup(popup) { - return document.getElementById(popup.id.substr(5)) + // return document.getElementById(popup.id.substr(5)) + return find_block(popup.closest('[data-uid]').dataset.uid) } /* popup component => coresponding event component */ function popup_from_event(event) { - return document.getElementById("popup" + event.id); + // return document.getElementById("popup" + event.id); + return find_popup(event.closest('[data-uid]').dataset.uid) } /* hides given popup */ @@ -17,7 +19,7 @@ function close_popup(popup) { /* hides all popups */ function close_all_popups () { - for (let popup of document.querySelectorAll(".popup-container.visible")) { + for (let popup of document.querySelectorAll("popup-element.visible")) { close_popup(popup); } } @@ -51,8 +53,8 @@ function open_popup(popup) { } /* toggles open/closed status of popup given by id */ -function toggle_popup(popup_id) { - let popup = document.getElementById(popup_id); +function toggle_popup(popup) { + // let popup = document.getElementById(popup_id); if (popup.classList.contains("visible")) { close_popup(popup); } else { diff --git a/static/script.js b/static/script.js index b4fb7bda..5ef498f3 100644 --- a/static/script.js +++ b/static/script.js @@ -4,211 +4,216 @@ 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 fractionsb -// 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); + + // document.createElement('vevent-block'); + + /* -------------------- */ + /* 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 fractionsb + 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; + that.event = document.createElement('vevent-block'); + + /* 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); + + } + } +} @@ -316,9 +321,9 @@ window.addEventListener('load', function () { } } - for (let nav of document.getElementsByClassName("popup-control")) { - bind_popup_control(nav); - } + // for (let nav of document.getElementsByClassName("popup-control")) { + // bind_popup_control(nav); + // } for (let el of document.getElementsByClassName("event")) { /* Popup script replaces need for anchors to events. diff --git a/static/style.scss b/static/style.scss index a29bb24b..4b4f573b 100644 --- a/static/style.scss +++ b/static/style.scss @@ -744,6 +744,21 @@ along with their colors. ---------------------------------------- */ +popup-element { + display: none; + position: absolute; + z-index: 1000; + + /* ??? */ + left: 10px; + top: -50px; + + box-shadow: gray 10px 10px 10px; + + &.visible { + display: block; + } +} .popup { display: flex; @@ -757,21 +772,6 @@ along with their colors. min-width: 60ch; min-height: 30ch; - &-container { - display: none; - position: absolute; - z-index: 1000; - - /* ??? */ - left: 10px; - top: -50px; - - box-shadow: gray 10px 10px 10px; - - &.visible { - display: block; - } - } input { white-space: initial; -- cgit v1.2.3 From 4cf9587a5188e5853bbcf97b71109e7cb9331d9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Sun, 3 Oct 2021 21:54:30 +0200 Subject: work --- module/calp/html/vcomponent.scm | 19 +-- module/calp/html/view/calendar.scm | 15 +-- module/calp/html/view/calendar/week.scm | 25 ++-- static/globals.js | 30 ++++- static/script.js | 100 +++++++-------- static/style.scss | 3 + static/vevent.js | 214 ++++++++++++++++++++++++++++++++ 7 files changed, 322 insertions(+), 84 deletions(-) create mode 100644 static/vevent.js diff --git a/module/calp/html/vcomponent.scm b/module/calp/html/vcomponent.scm index c5ae452d..1b97e9b8 100644 --- a/module/calp/html/vcomponent.scm +++ b/module/calp/html/vcomponent.scm @@ -22,6 +22,7 @@ format-summary format-recurrence-rule )) + :use-module ((calp util config) :select (get-config)) ) (define-public (compact-event-list list) @@ -203,11 +204,19 @@ `(vevent-edit (@ (class "vevent") (data-uid ,(prop ev 'UID))))) -(define-public (edit-template) +(define-public (edit-template calendars) `(div (@ (class " eventtext edit-tab ")) (form (@ (class "edit-form")) - ;; TODO actually have calendar list here, since we are just a template - (div (@ (class "dropdown-goes-here"))) + (select (@ (class "calendar-selection")) + (option "- Choose a Calendar -") + ,@(let ((dflt (get-config 'default-calendar))) + (map (lambda (calendar) + (define name (prop calendar 'NAME)) + `(option (@ (value ,(html-attr name)) + ,@(when (string=? name dflt) + '((selected)))) + ,name)) + calendars))) (h3 (input (@ (type "text") (placeholder "Sammanfattning") (name "summary") (required) @@ -605,10 +614,6 @@ `(("📅" title: "Redigera" ,(fmt-for-edit ev)))) - ,@(when (debug) - `(("🐸" title: "Debug" - (div - (pre ,(prop ev 'UID)))))) ("⤓" title: "Nedladdning" (div (@ (class "eventtext") (style "font-family:sans")) diff --git a/module/calp/html/view/calendar.scm b/module/calp/html/view/calendar.scm index f447773d..dfcd2264 100644 --- a/module/calp/html/view/calendar.scm +++ b/module/calp/html/view/calendar.scm @@ -269,18 +269,9 @@ (or (prop (parent event) 'NAME) "")))))))) ,(prop calendar 'NAME)))) calendars)) - (div (@ (id "calendar-dropdown-template") (class "template")) - (select - (option "- Choose a Calendar -") - ,@(let ((dflt (get-config 'default-calendar))) - (map (lambda (calendar) - (define name (prop calendar 'NAME)) - `(option (@ (value ,(html-attr name)) - ,@(when (string=? name dflt) - '((selected)))) - ,name)) - calendars))) - ))) + ;; (div (@ (id "calendar-dropdown-template") (class "template")) + ;; ) + )) ;; List of events (div (@ (class "eventlist") diff --git a/module/calp/html/view/calendar/week.scm b/module/calp/html/view/calendar/week.scm index 5361ab65..9911b162 100644 --- a/module/calp/html/view/calendar/week.scm +++ b/module/calp/html/view/calendar/week.scm @@ -22,7 +22,7 @@ ) -(define*-public (render-calendar key: events start-date end-date #:allow-other-keys) +(define*-public (render-calendar key: calendars events start-date end-date #:allow-other-keys) (let* ((long-events short-events (partition long-event? (stream->list (events-between start-date end-date events)))) (range (date-range start-date end-date))) `((script "const VIEW='week';") @@ -56,6 +56,7 @@ ,@(for event in (stream->list (events-between start-date end-date events)) `(popup-element + ;; TODO (@ (class "vevent") (data-uid ,(prop event 'UID))) ) @@ -72,7 +73,9 @@ ;; edit tab of popup (template (@ (id "vevent-edit")) - ,((@ (calp html vcomponent) edit-template))) + ,((@ (calp html vcomponent) edit-template) + calendars + )) ;; "physical" block (template (@ (id "vevent-block")) @@ -83,11 +86,11 @@ (template (@ (id "popup-template")) (div (@ ; (id ,id) - (class "popup-container CAL_" - #; - ,(html-attr (or (prop (parent ev) 'NAME) ; - "unknown"))) - (onclick "event.stopPropagation()")) + (class "popup-container CAL_" + #; + ,(html-attr (or (prop (parent ev) 'NAME) ; + "unknown"))) + (onclick "event.stopPropagation()")) (div (@ (class "popup")) (nav (@ (class "popup-control")) ,(btn "×" @@ -98,13 +101,15 @@ ,(tabset `(("📅" title: "Översikt" - (vevent-description (@ (class "populate-with-uid"))) + (vevent-description + (@ (class "vevent populate-with-uid"))) ) ,@(when (edit-mode) `(("📅" title: "Redigera" - (vevent-edit (@ (class "populate-with-uid")))))))))) - ) + (vevent-edit (@ (class "populate-with-uid")))))) + + ))))) ))) diff --git a/static/globals.js b/static/globals.js index 41472264..86368e9a 100644 --- a/static/globals.js +++ b/static/globals.js @@ -7,6 +7,11 @@ class ComponentVEvent extends HTMLElement { super (); this.template = document.getElementById(this.tagName); + let uid; + if ((uid = this.dataset.uid)) { + vcal_objects[uid].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. Those that need a direct rerendering (such as the edit tabs) @@ -122,12 +127,13 @@ class ComponentEdit extends ComponentVEvent { } function find_popup (uid) { - for (let el of vcal_objects[uid].registered) { - if (el.tagName === 'popup-element') { - return el; - } - } - throw 'Popup not fonud'; + // 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}"]`) } function find_block (uid) { @@ -180,6 +186,7 @@ window.addEventListener('load', function () { - .block - .list */ + /* let vevent_els = document.getElementsByClassName('vevent') for (let el of vevent_els) { try { @@ -190,6 +197,7 @@ window.addEventListener('load', function () { ); } } + */ customElements.define('vevent-description', ComponentDescription); customElements.define('vevent-edit', ComponentEdit); @@ -305,6 +313,16 @@ class PopupElement extends HTMLElement { /* 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; + // event.properties.calendar = this.value; + }); + } } diff --git a/static/script.js b/static/script.js index 5ef498f3..16ff7bbd 100644 --- a/static/script.js +++ b/static/script.js @@ -28,14 +28,14 @@ class EventCreator { /* 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); - } + // 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 */ - } + // popup.getElementsByClassName("edit-form")[0].onsubmit = function () { + // create_event(event); + // return false; /* stop default */ + // } /* -------------------- */ /* Fix tabs for newly created popup */ @@ -128,12 +128,12 @@ class EventCreator { */ let time = round_time(pos_in(this, e), round_to); - event.dataset.time1 = time; - event.dataset.time2 = time; + that.event.dataset.time1 = time; + that.event.dataset.time2 = time; /* ---------------------------------------- */ - this.appendChild(event); + this.appendChild(that.event); /* requires that event is child of an '.event-container'. */ // new VComponent( @@ -186,9 +186,11 @@ class EventCreator { 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; + console.log(d1.format("~L~H:~M"), d2.format("~L~H:~M")); + that.event.redraw({ getProperty: (name) => + ({ dtstart: d1, dtend: d2 })[name]}); + // that.event.properties.dtstart = d1; + // that.event.properties.dtend = d2; } } @@ -204,7 +206,7 @@ class EventCreator { e.style.pointerEvents = ""; } - place_in_edit_mode(that.event); + // place_in_edit_mode(that.event); let localevent = that.event; that.event = null; @@ -222,40 +224,40 @@ class EventCreator { TODO stop requiring a weird button press to change calendar. */ -function place_in_edit_mode (event) { - let popup = document.getElementById("popup" + event.id) - let container = popup.getElementsByClassName('dropdown-goes-here')[0] - let calendar_dropdown = document.getElementById('calendar-dropdown-template').firstChild.cloneNode(true); - - let [_, calclass] = popup.classList.find(/^CAL_/); - label: { - for (let [i, option] of calendar_dropdown.childNodes.entries()) { - if (option.value === calclass.substr(4)) { - calendar_dropdown.selectedIndex = i; - break label; - } - } - /* no match, try find default calendar */ - let t; - if ((t = calendar_dropdown.querySelector("[selected]"))) { - event.properties.calendar = t.value; - } - } - - - /* Instant change while user is stepping through would be - * preferable. But I believe that