diff options
author | Hugo Hörnquist <hugo@lysator.liu.se> | 2021-10-03 21:54:30 +0200 |
---|---|---|
committer | Hugo Hörnquist <hugo@lysator.liu.se> | 2021-10-03 21:54:30 +0200 |
commit | 4cf9587a5188e5853bbcf97b71109e7cb9331d9f (patch) | |
tree | f6f36930e263f3fee450a700b8e77da667c1afa9 | |
parent | Further work, rework popup. (diff) | |
download | calp-4cf9587a5188e5853bbcf97b71109e7cb9331d9f.tar.gz calp-4cf9587a5188e5853bbcf97b71109e7cb9331d9f.tar.xz |
work
-rw-r--r-- | module/calp/html/vcomponent.scm | 19 | ||||
-rw-r--r-- | module/calp/html/view/calendar.scm | 15 | ||||
-rw-r--r-- | module/calp/html/view/calendar/week.scm | 25 | ||||
-rw-r--r-- | static/globals.js | 30 | ||||
-rw-r--r-- | static/script.js | 100 | ||||
-rw-r--r-- | static/style.scss | 3 | ||||
-rw-r--r-- | static/vevent.js | 214 |
7 files changed, 322 insertions, 84 deletions
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 <option> first gives us the - * input once selected */ - calendar_dropdown.onchange = function () { - event.properties.calendar = this.value; - } - container.appendChild(calendar_dropdown); - - let tab = popup.getElementsByClassName("tab")[1]; - let radio = tab.getElementsByTagName("input")[0]; - radio.click(); - tab.querySelector("input[name='summary']").focus(); -} +// 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 <option> first gives us the +// * input once selected */ +// calendar_dropdown.onchange = function () { +// event.properties.calendar = this.value; +// } +// container.appendChild(calendar_dropdown); +// +// let tab = popup.getElementsByClassName("tab")[1]; +// let radio = tab.getElementsByTagName("input")[0]; +// radio.click(); +// tab.querySelector("input[name='summary']").focus(); +// } @@ -277,7 +279,7 @@ window.addEventListener('load', function () { }, 1000 * 60); /* Is event creation active? */ - if (false && EDIT_MODE) { + if (true && EDIT_MODE) { let eventCreator = new EventCreator; for (let c of document.getElementsByClassName("events")) { c.onmousedown = eventCreator.create_event_down(c); diff --git a/static/style.scss b/static/style.scss index 4b4f573b..cc1ae15b 100644 --- a/static/style.scss +++ b/static/style.scss @@ -772,6 +772,9 @@ popup-element { min-width: 60ch; min-height: 30ch; + select { + max-width: 100%; + } input { white-space: initial; diff --git a/static/vevent.js b/static/vevent.js new file mode 100644 index 00000000..678f2134 --- /dev/null +++ b/static/vevent.js @@ -0,0 +1,214 @@ +"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 */ + console.log(el); + 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 'utc-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) +} |