From 228c86f792dcb487c923e173c90c995acc09efbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Sun, 27 Sep 2020 17:17:55 +0200 Subject: Add new edit tab. --- module/calp/html/vcomponent.scm | 72 ++++++++++++++++++++++++++++++++++++++--- static/style.scss | 4 +++ 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/module/calp/html/vcomponent.scm b/module/calp/html/vcomponent.scm index c4e15374..8e52ed7c 100644 --- a/module/calp/html/vcomponent.scm +++ b/module/calp/html/vcomponent.scm @@ -98,17 +98,74 @@ ,(string-map (lambda (c) (if (char=? c #\,) #\newline c)) (prop ev 'LOCATION))))) ,(awhen (prop ev 'DESCRIPTION) - `(span (@ (class "description")) + `(div (@ (class "description")) ,(format-description ev it))) + + ,(awhen (prop ev 'CATEGORIES) + `(div (@ (class "categories")) + ,@(map (lambda (c) + `(a (@ (class "category") + ;; TODO centralize search terms + ;; TODO propper stringifycation of sexp + (href ,(format #f "/search/?q=%28member+%22~a%22%0D%0A++%28or+%28prop+event+%27CATEGORIES%29+%27%28%29%29%0D%0A" + c))) + ,c)) + it))) ,(awhen (prop ev 'RRULE) - `(span (@ (class "rrule")) - ,@(format-recurrence-rule ev))) + `(div (@ (class "rrule")) + ,@(format-recurrence-rule ev))) + ,(when (prop ev 'LAST-MODIFIED) - `(span (@ (class "last-modified")) "Senast ändrad " - ,(datetime->string (prop ev 'LAST-MODIFIED) "~1 ~H:~M")))) + `(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 ")) + (h3 (input (@ (type "text") (class "summary") + (placeholder "Sammanfattning") + (name "summary") (required) + (value ,(prop ev 'SUMMARY))))) + (div + ,(let ((start (prop ev 'DTSTART)) + (end (prop ev 'DTEND))) + `(table + (tr (td "Heldag?") + (td (input (@ (type "checkbox") + ,@(when (date? start) '((checked))))))) + (tr (td "Start") + (td (input (@ (type "date") (value ,(date->string (as-date start)))))) + (td + (input (@ ,@(when (date? start) + '((style "display:none"))) + (type "time") + (value ,(time->string (as-time start))))))) + (tr (td "Slut") + (td (input (@ (type "date") + ,@(when end `((value ,(date->string (as-date end)))))))) + (td (input (@ ,@(when (date? start) + '((style "display:none"))) + (type "time") + ,@(when end `((value ,(time->string (as-time end))))) + ))))))) + + (div (b "Plats: ") + (input (@ (name "location") + (value ,(or (prop ev 'LOCATION) ""))))) + + (div (@ (class "description")) + (textarea ,(prop ev 'DESCRIPTION))) + + (div (@ (class "categories")) + ,@(awhen (prop ev 'CATEGORIES) + (map (lambda (c) `(button (@ (class "category")) ,c)) + it)) + + (input (@ (class "category") (type "text") (placeholder "category")))))) + ;; Single event in side bar (text objects) (define-public (fmt-day day) @@ -238,6 +295,10 @@ ,(tabset `(("📅" title: "Översikt" ,(fmt-single-event ev)) + + ("📅" title: "Redigera" + ,(fmt-for-edit ev)) + ("⤓" title: "Nedladdning" (div (@ (class "eventtext") (style "font-family:sans")) (h2 "Ladda ner") @@ -245,6 +306,7 @@ "som iCal")) (li (a (@ (href "/calendar/" ,(prop ev 'UID) ".xcs")) "som xCal"))))) + ,@(when (prop ev 'RRULE) `(("↺" title: "Upprepningar" class: "repeating" ,(repeat-info ev))))))))) diff --git a/static/style.scss b/static/style.scss index 33f55f81..3a2e5ba6 100644 --- a/static/style.scss +++ b/static/style.scss @@ -549,6 +549,10 @@ along with their colors. font-style: italic; white-space: pre; } + + .category { + display: inline-block; + } } .event-body { -- cgit v1.2.3 From 7c4dda4f90a1929dde13b254ae6f1e5a766cc7f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Sun, 27 Sep 2020 20:53:35 +0200 Subject: Input cleaned up. --- module/calp/html/components.scm | 50 +++++++++++++++++++++++ module/calp/html/vcomponent.scm | 87 +++++++++++++++++++++++------------------ static/style.scss | 19 +++++++-- 3 files changed, 116 insertions(+), 40 deletions(-) diff --git a/module/calp/html/components.scm b/module/calp/html/components.scm index ebc359b8..03e1cef1 100644 --- a/module/calp/html/components.scm +++ b/module/calp/html/components.scm @@ -1,6 +1,8 @@ (define-module (calp html components) :use-module (calp util) :use-module (calp util exceptions) + :use-module (ice-9 curried-definitions) + :use-module (ice-9 match) :export (xhtml-doc) ) @@ -112,6 +114,54 @@ ,key) (div (@ (class "content")) ,body))))) +(define ((set-attribute attr) el) + (match el + [(tagname ('@ params ...) inner-body ...) + `(,tagname (@ ,@(assq-merge params attr)) + ,@inner-body)] + [(tagname inner-body ...) + `(,tagname (@ ,attr) + ,@inner-body)])) + + +(define-public (with-label lbl . forms) + + (define id (gensym "label")) + + (cons `(label (@ (for ,id)) ,lbl) + (let recurse ((forms forms)) + (map (lambda (form) + (cond [(not (list? form)) form] + [(null? form) '()] + [(eq? 'input (car form)) + ((set-attribute `((id ,id))) form)] + [(list? (car form)) + (cons (recurse (car form)) + (recurse (cdr form)))] + [else + (cons (car form) + (recurse (cdr form)))])) + forms)))) + + +(define-public (form elements) + `(form + ,@(map (label self + (lambda (el) + (match el + ((name ('@ tags ...) body ...) + (let ((id (gensym "formelement"))) + (cons + `(label (@ (for ,id)) ,name) + (map + (set-attribute `((name ,name))) + (cons + ((set-attribute `((id ,id))) (car body)) + (cdr body)))))) + ((name body ...) + (self `(,name (@) ,@body)))))) + elements))) + (define-public (include-css path . extra-attributes) `(link (@ (type "text/css") diff --git a/module/calp/html/vcomponent.scm b/module/calp/html/vcomponent.scm index 8e52ed7c..89020bd8 100644 --- a/module/calp/html/vcomponent.scm +++ b/module/calp/html/vcomponent.scm @@ -7,7 +7,7 @@ :use-module ((text util) :select (add-enumeration-punctuation)) :use-module (calp html util) :use-module ((calp html config) :select (edit-mode)) - :use-module ((calp html components) :select (btn tabset)) + :use-module ((calp html components) :select (btn tabset form with-label)) :use-module ((calp util color) :select (calculate-fg-color)) :use-module ((vcomponent datetime output) :select (fmt-time-span @@ -125,46 +125,59 @@ optional: (attributes '()) key: (fmt-header list)) `(div (@ (class " eventtext ")) - (h3 (input (@ (type "text") (class "summary") - (placeholder "Sammanfattning") - (name "summary") (required) - (value ,(prop ev 'SUMMARY))))) - (div - ,(let ((start (prop ev 'DTSTART)) - (end (prop ev 'DTEND))) - `(table - (tr (td "Heldag?") - (td (input (@ (type "checkbox") - ,@(when (date? start) '((checked))))))) - (tr (td "Start") - (td (input (@ (type "date") (value ,(date->string (as-date start)))))) - (td - (input (@ ,@(when (date? start) - '((style "display:none"))) - (type "time") - (value ,(time->string (as-time start))))))) - (tr (td "Slut") - (td (input (@ (type "date") - ,@(when end `((value ,(date->string (as-date end)))))))) - (td (input (@ ,@(when (date? start) - '((style "display:none"))) - (type "time") - ,@(when end `((value ,(time->string (as-time end))))) - ))))))) + (div (@ (class "edit-form")) + (h3 (input (@ (type "text") (class "summary") + (placeholder "Sammanfattning") + (name "summary") (required) + (value ,(prop ev 'SUMMARY))))) + + ,@(with-label "Heldag" `(input (@ (name "wholeday") (type "checkbox")))) + + ,@(let ((start (prop ev 'DTSTART))) + (with-label "Start" + `(div (input (@ (type "date") + (name "dtstart-date") + (value ,(date->string (as-date start))))) + (input (@ ,@(when (date? start) + '((style "display:none"))) + (type "time") + (name "dtstart-end") + (value ,(time->string (as-time start)))))))) + ,@(let ((end (prop ev 'DTEND))) + (with-label "Slut" + `(div (input (@ (type "date") + (name "dtend-date") + ,@(when end `((value ,(date->string (as-date end))))))) + (input (@ ,@(when (date? end) + '((style "display:none"))) + (type "time") + (name "dtend-time") + ,@(when end `((value ,(time->string (as-time end))))) + ))))) - (div (b "Plats: ") - (input (@ (name "location") - (value ,(or (prop ev 'LOCATION) ""))))) + ,@(with-label + "Plats" + `(input (@ (placeholder "Plats") + (name "location") + (type "text") + (value ,(or (prop ev 'LOCATION) ""))))) - (div (@ (class "description")) - (textarea ,(prop ev 'DESCRIPTION))) + ,@(with-label + "Beskrivning" + `(textarea (@ (placeholder "Beskrivning") + (name "description")) + ,(prop ev 'DESCRIPTION))) - (div (@ (class "categories")) - ,@(awhen (prop ev 'CATEGORIES) - (map (lambda (c) `(button (@ (class "category")) ,c)) - it)) + ,@(with-label + "Kategorier" + (awhen (prop ev 'CATEGORIES) + (map (lambda (c) `(button (@ (class "category")) ,c)) + it)) - (input (@ (class "category") (type "text") (placeholder "category")))))) + `(input (@ (class "category") + (type "text") + (placeholder "category")))) + ))) ;; Single event in side bar (text objects) diff --git a/static/style.scss b/static/style.scss index 3a2e5ba6..24f88f48 100644 --- a/static/style.scss +++ b/static/style.scss @@ -698,8 +698,8 @@ along with their colors. /* overflow-y: auto; */ max-width: 60ch; max-height: 60ch; - min-width: 40ch; - min-height: 20ch; + min-width: 60ch; + min-height: 30ch; &-container { display: none; @@ -790,7 +790,7 @@ along with their colors. } .tab { - label { + > label { position: absolute; left: 100%; @@ -837,6 +837,19 @@ along with their colors. min-width: 100%; min-height: 100%; } + + + .edit-form { + label { + display: block; + } + + input[type='text'], textarea { + width: 100%; + } + } + + } /* Other -- cgit v1.2.3 From b3250ac8289e4f4154682680d89c417f9a115e18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Sun, 27 Sep 2020 23:17:01 +0200 Subject: Add fancy editing of tag list. --- module/calp/html/vcomponent.scm | 20 +++++++++------ static/script.js | 54 +++++++++++++++++++++++++++++++++++++++++ static/style.scss | 9 +++++++ 3 files changed, 76 insertions(+), 7 deletions(-) diff --git a/module/calp/html/vcomponent.scm b/module/calp/html/vcomponent.scm index 89020bd8..67634492 100644 --- a/module/calp/html/vcomponent.scm +++ b/module/calp/html/vcomponent.scm @@ -131,7 +131,7 @@ (name "summary") (required) (value ,(prop ev 'SUMMARY))))) - ,@(with-label "Heldag" `(input (@ (name "wholeday") (type "checkbox")))) + ,@(with-label "Heldag?" `(input (@ (name "wholeday") (type "checkbox")))) ,@(let ((start (prop ev 'DTSTART))) (with-label "Start" @@ -170,13 +170,19 @@ ,@(with-label "Kategorier" - (awhen (prop ev 'CATEGORIES) - (map (lambda (c) `(button (@ (class "category")) ,c)) - it)) + `(div (@ (class "inline-edit")) + ,@(awhen (prop ev 'CATEGORIES) + (map (lambda (c) + `(input (@ (size 2) + (value ,c)))) + it)) - `(input (@ (class "category") - (type "text") - (placeholder "category")))) + (input (@ (class "final") + (size 2) + (type "text") + )))) + + (input (@ (type "submit"))) ))) diff --git a/static/script.js b/static/script.js index ce57e5e1..069d9a2e 100644 --- a/static/script.js +++ b/static/script.js @@ -623,6 +623,11 @@ window.onload = function () { serializer.serializeToString(xml); */ + + for (let el of document.querySelectorAll(".inline-edit input")) { + el.oninput = update_inline_list; + } + } function close_popup(popup) { @@ -716,6 +721,25 @@ function get_property(el, field, default_value, bind_to_ical=true) { } + +/* +class display_tab { +} + +class edit_tab { +} + +class vcomponent { + set_value(field, value) { + if (value === '') { + remove_property(field); + } + } +} + + +*/ + /* Properties are icalendar properties. @@ -804,4 +828,34 @@ function bind_properties (el, wide_event=false) { calprop.push([el, rplcs]); calprop.push([el, (s, v) => s.dataset.calendar = v]); + + + /* ---------- Calendar ------------------------------ */ + + +} + + +function advance_final(li) { + li.classList.remove("final"); + let new_li = makeElement ('input', { + className: 'final', + size: 2, + oninput: li.oninput, + }); + li.closest(".inline-edit").appendChild(new_li); +} + +function update_inline_list () { + if (this.classList.contains("final")) { + if (this.value !== '') { + advance_final(this); + } + } else { + if (this.value === '') { + let sibling = this.previousElementSibling || this.nextElementSibling; + this.remove(); + sibling.focus(); + } + } } diff --git a/static/style.scss b/static/style.scss index 24f88f48..736f170a 100644 --- a/static/style.scss +++ b/static/style.scss @@ -844,6 +844,7 @@ along with their colors. display: block; } + /* REG */ input[type='text'], textarea { width: 100%; } @@ -852,6 +853,14 @@ along with their colors. } +.inline-edit { + input { + /* important since regular spec is much stronger...*/ + /* [REG] */ + width: initial !important; + } +} + /* Other ---------------------------------------- */ -- cgit v1.2.3 From 6ad74ec3383b7a8d8403cdf185caabd4332109a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Mon, 28 Sep 2020 01:01:45 +0200 Subject: Made timeinput checkbox needlesly fancy. --- module/calp/html/vcomponent.scm | 134 ++++++++++++++++++++++------------------ static/style.scss | 37 ++++++++++- 2 files changed, 109 insertions(+), 62 deletions(-) diff --git a/module/calp/html/vcomponent.scm b/module/calp/html/vcomponent.scm index 67634492..54bfb9e8 100644 --- a/module/calp/html/vcomponent.scm +++ b/module/calp/html/vcomponent.scm @@ -125,65 +125,81 @@ optional: (attributes '()) key: (fmt-header list)) `(div (@ (class " eventtext ")) - (div (@ (class "edit-form")) - (h3 (input (@ (type "text") (class "summary") - (placeholder "Sammanfattning") - (name "summary") (required) - (value ,(prop ev 'SUMMARY))))) - - ,@(with-label "Heldag?" `(input (@ (name "wholeday") (type "checkbox")))) - - ,@(let ((start (prop ev 'DTSTART))) - (with-label "Start" - `(div (input (@ (type "date") - (name "dtstart-date") - (value ,(date->string (as-date start))))) - (input (@ ,@(when (date? start) - '((style "display:none"))) - (type "time") - (name "dtstart-end") - (value ,(time->string (as-time start)))))))) - ,@(let ((end (prop ev 'DTEND))) - (with-label "Slut" - `(div (input (@ (type "date") - (name "dtend-date") - ,@(when end `((value ,(date->string (as-date end))))))) - (input (@ ,@(when (date? end) - '((style "display:none"))) - (type "time") - (name "dtend-time") - ,@(when end `((value ,(time->string (as-time end))))) - ))))) - - ,@(with-label - "Plats" - `(input (@ (placeholder "Plats") - (name "location") - (type "text") - (value ,(or (prop ev 'LOCATION) ""))))) - - ,@(with-label - "Beskrivning" - `(textarea (@ (placeholder "Beskrivning") - (name "description")) - ,(prop ev 'DESCRIPTION))) - - ,@(with-label - "Kategorier" - `(div (@ (class "inline-edit")) - ,@(awhen (prop ev 'CATEGORIES) - (map (lambda (c) - `(input (@ (size 2) - (value ,c)))) - it)) - - (input (@ (class "final") - (size 2) - (type "text") - )))) - - (input (@ (type "submit"))) - ))) + (form (@ (class "edit-form")) + (h3 (input (@ (type "text") + (placeholder "Sammanfattning") + (name "summary") (required) + (value ,(prop ev 'SUMMARY))))) + + ,(let ((start (prop ev 'DTSTART)) + (end (prop ev 'DTEND))) + `(div (@ (class "timeinput")) + + (input (@ (type "date") + (name "dtstart-date") + (style "grid-column:1;grid-row:2") + (value ,(date->string (as-date start))))) + + (input (@ (type "date") + (name "dtend-date") + (style "grid-column:1;grid-row:3") + ,@(when end `((value ,(date->string (as-date end))))))) + + ,@(with-label + "Heldag?" + `(input (@ (type "checkbox") (style "display:none") + (name "wholeday")))) + + (input (@ ,@(when (date? start) + '((style "display:none"))) + (type "time") + (name "dtstart-end") + (style "grid-column:3;grid-row:2") + (value ,(time->string (as-time start))))) + + (input (@ ,@(when (date? end) + '((style "display:none"))) + (type "time") + (name "dtend-time") + (style "grid-column:3;grid-row:3") + ,@(when end `((value ,(time->string (as-time end))))) + )))) + + ,@(with-label + "Plats" + `(input (@ (placeholder "Plats") + (name "location") + (type "text") + (value ,(or (prop ev 'LOCATION) ""))))) + + ,@(with-label + "Beskrivning" + `(textarea (@ (placeholder "Beskrivning") + (name "description")) + ,(prop ev 'DESCRIPTION))) + + ,@(with-label + "Kategorier" + `(div (@ (class "inline-edit")) + ,@(awhen (prop ev 'CATEGORIES) + (map (lambda (c) + `(input (@ (size 2) + (value ,c)))) + it)) + + (input (@ (class "final") + (size 2) + (type "text") + )))) + + #; + (input (@ (type "text") + (list "known-fields") + (placeholder "Nytt fält"))) + + + (input (@ (type "submit"))) + ))) ;; Single event in side bar (text objects) diff --git a/static/style.scss b/static/style.scss index 736f170a..c2848940 100644 --- a/static/style.scss +++ b/static/style.scss @@ -704,7 +704,7 @@ along with their colors. &-container { display: none; position: absolute; - z-index: 10; + z-index: 1000; /* ??? */ left: 10px; @@ -814,13 +814,13 @@ along with their colors. [type=radio] { display: none; &:checked ~ label { - z-index: 1; + z-index: 100; /* to align all tab */ border-left: 3px solid transparent; background-color: #dedede; ~ .content { - z-index: 1; + z-index: 100; } } } @@ -848,6 +848,37 @@ along with their colors. input[type='text'], textarea { width: 100%; } + + .timeinput { + + display: grid; + grid-template-columns: 1fr [lbl-start] 1ch 1fr 1ch [lbl-end]; + grid-template-rows: [lbl-start] 0.7fr 1fr 1fr 0.3fr [lbl-end]; + + label { + background-color: rgba(10,20,30,0.7); + color: white; + border-radius: 1em; + + grid-column: lbl-start / lbl-end; + grid-row: lbl-start / lbl-end; + + text-align: center; + + user-select: none; + + z-index: 1; + + } + + input { + z-index: 2; + } + + input:checked ~ input { + z-index: 0; + } + } } -- cgit v1.2.3 From 04dcab7a429d9b034d41b5aca8bd715c4826de32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Mon, 28 Sep 2020 03:10:54 +0200 Subject: Groundwork for adding new fields from frontend. --- module/calp/html/vcomponent.scm | 17 ++- module/calp/html/view/calendar.scm | 25 +++- static/script.js | 239 ++++++++++++++++++++++++++++++++++++- static/style.scss | 12 ++ 4 files changed, 287 insertions(+), 6 deletions(-) diff --git a/module/calp/html/vcomponent.scm b/module/calp/html/vcomponent.scm index 54bfb9e8..2497aa04 100644 --- a/module/calp/html/vcomponent.scm +++ b/module/calp/html/vcomponent.scm @@ -192,10 +192,19 @@ (type "text") )))) - #; - (input (@ (type "text") - (list "known-fields") - (placeholder "Nytt fält"))) + (hr) + + (div (@ (class "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.scm b/module/calp/html/view/calendar.scm index a583d82b..64986b5c 100644 --- a/module/calp/html/view/calendar.scm +++ b/module/calp/html/view/calendar.scm @@ -296,4 +296,27 @@ ;; 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))))))))) + ,(popup event (string-append "popup" (html-id event)))))) + + ;; Auto-complets when adding new fields to a component + ;; Any string is however still valid. + (datalist (@ (id "known-fields")) + ,@(map (lambda (f) + `(option (@ (value ,f)))) + '(CALSCALE + METHOD PRODID VERSION ATTACH + CATEGORIES CLASS COMMENT + DESCRIPTION GEO LOCATION + PERCENT-COMPLETE PRIORITY + RESOURCES STATUS SUMMARY + COMPLETED DTEND DUE DTSTART + DURATION FREEBUSY + TRANSP TZID TZNAME + TZOFFSETFROM TZOFFSETTO + TZURL ATTENDEE CONTACT + ORGANIZER RECURRENCE-ID + RELATED-TO URL EXDATE + RDATE RRULE ACTION REPEAT + TRIGGER CREATED DTSTAMP LAST-MODIFIED + SEQUENCE REQUEST-STATUS + )))))) diff --git a/static/script.js b/static/script.js index 069d9a2e..b94d0f2b 100644 --- a/static/script.js +++ b/static/script.js @@ -628,6 +628,222 @@ window.onload = function () { el.oninput = update_inline_list; } + + for (let el of document.getElementsByClassName("newfield")) { + let [name, type_selector, value_field] = el.children; + + /* TODO list fields */ + /* TODO add and remove fields. See update_inline_list */ + + function update_value_field (el) { + let [name_field, type_selector, value_field] = el.children; + + + let value = makeElement('input'); + let values = [value]; + + + switch (name_field.value.toUpperCase()) { + case 'GEO': + value.type = 'number'; + values.push(makeElement('input', { + type: 'number', + })); + break; + + case 'CLASS': + // Add auto completion + break; + + case 'ACTION': + // Add auto completion + break; + + case 'TRANSP': + // Replace with toggle betwen OPAQUE and TRANSPARENT + break; + + case 'PERCENT-COMPLETE': + value.min = 0; + value.max = 100; + break; + + case 'PRIORITY': + value.min = 0; + value.max = 9; + break; + + default: + + + switch (type_selector.options[type_selector.selectedIndex].value) { + case 'integer': + case 'float': + value.type = 'number'; + break; + + case 'uri': + value.type = 'url'; + break; + + case 'binary': + value.type = 'file'; + break; + + case 'date-time': + values.push(makeElement('input', { + type: 'time', + })); + /* fallthrough */ + case 'date': + value.type = 'date'; + break; + + case 'cal-address': + value.type = 'email'; + break; + + case 'utc-offset': + value.type = 'time'; + let lbl = makeElement('label'); + let id = gensym(); + + lbl.setAttribute('for', id); + + /* TODO make these labels stand out more */ + lbl.appendChild(makeElement('span', { + className: 'plus', + innerText: '+', + })); + lbl.appendChild(makeElement('span', { + className: 'minus', + innerText: '-', + })); + values.splice(0,0,lbl); + values.splice(0,0, makeElement('input', { + type: 'checkbox', + style: 'display:none', + className: 'plusminuscheck', + id: id, + })); + break; + + case 'period': + value.type = 'text'; + // TODO validate /P\d*H/ typ + break; + + case 'recur': + // TODO + default: + value.type = 'text'; + } + } + + + value_field.innerHTML = ''; + for (let v of values) { + console.log(v); + value_field.appendChild(v); + } + } + + name.oninput = function () { + let types = valid_input_types[this.value.toUpperCase()]; + type_selector.disabled = false; + if (types) { + type_selector.innerHTML = ''; + for (let type of types) { + type_selector.appendChild( + makeElement('option', { value: type, innerText: type })) + } + if (types.length == 1) { + type_selector.disabled = true; + } + } else { + type_selector.innerHTML = ''; + for (let type of all_types) { + type_selector.appendChild( + makeElement('option', { value: type, innerText: type })) + } + } + + update_value_field(el); + } + type_selector.onchange = function () { + update_value_field(el); + } + } + +} + +let all_types = [ + 'text', + 'uri', + 'binary', + 'float', + 'integer', + 'date-time', + 'date', + 'duration', + 'period', + 'utc-offset', + 'cal-address', + 'recur', +] + + +/* TODO + Todo map for which properties are valid on which object types +*/ + + +let valid_input_types = { + 'CALSCALE': ['text'], + 'METHOD': ['text'], + 'PRODID': ['text'], + 'VERSION': ['text'], + 'ATTACH': ['uri', 'binary'], + 'CATEGORIES': [['text']], + 'CLASS': ['text'], // PUBLIC|PRIVATE|CONFIDENTIAL|*other* + 'COMMENT': ['text'], + 'DESCRIPTION': ['text'], + 'GEO': ['float'], // pair of floats + 'LOCATION': ['text'], + 'PERCENT-COMPLETE': ['integer'], // 0-100 + 'PRIORITY': ['integer'], // 0-9 + 'RESOURCES': [['text']], + 'STATUS': ['text'], // see 3.8.1.11 + 'SUMMARY': ['text'], + 'COMPLETED': ['date-time'], + 'DTEND': ['date', 'date-time'], + 'DUE': ['date', 'date-time'], + 'DTSTART': ['date', 'date-time'], + 'DURATION': ['duration'], + 'FREEBUSY': [['period']], + 'TRANSP': ['text'], // OPAQUE|TRANSPARENT + 'TZID': ['text'], + 'TZNAME': ['text'], + 'TZOFFSETFROM': ['utc-offset'], + 'TZOFFSETTO': ['utc-offset'], + 'TZURL': ['uri'], + 'ATTENDEE': ['cal-address'], + 'CONTACT': ['text'], + 'ORGANIZER': ['cal-address'], + 'RECURRENCE-ID': ['date', 'date-time'], + 'RELATED-TO': ['text'], + 'URL': ['uri'], + 'EXDATE': [['date', 'date-time']], + 'RDATE': [['date', 'date-time', 'period']], + 'RRULE': ['recur'], + 'ACTION': ['text'], // AUDIO|DISPLAY|EMAIL|*other* + 'REPEAT': ['integer'], + 'TRIGGER': ['duration', 'date-time'], + 'CREATED': ['date-time'], + 'DTSTAMP': ['date-time'], + 'LAST-MODIFIED': ['date-time'], + 'SEQUENCE': ['integer'], + 'REQUEST-STATUS': ['text'] } function close_popup(popup) { @@ -754,7 +970,8 @@ class vcomponent { function bind_properties (el, wide_event=false) { el.properties = {} let popup = document.getElementById("popup" + el.id); - let children = el.getElementsByTagName("properties")[0].children; + // let children = el.getElementsByTagName("properties")[0].children; + let children = el.querySelector("vevent > properties").children; for (let child of children) { let field = child.tagName; @@ -771,6 +988,26 @@ function bind_properties (el, wide_event=false) { lst.push([s, f]); } + + /* edit tab */ + for (let s of popup.querySelectorAll(`[name='${field}']`)) { + let f; + s.oninput = function () { + el.properties[s.name] = this.value; + } + switch (s.name) { + case 'description': + f = ((s, v) => s.innerHTML = v.format(s.dataset && s.dataset.fmt)); + lst.push([s, f]); + break; + default: + f = ((s, v) => s.value = v); + lst.push([s, f]); + break; + } + } + + /* Bind vcomponent fields for this event */ for (let s of el.querySelectorAll(field + " > :not(parameters)")) { switch (s.tagName) { diff --git a/static/style.scss b/static/style.scss index c2848940..d69e8bcf 100644 --- a/static/style.scss +++ b/static/style.scss @@ -884,6 +884,18 @@ along with their colors. } +.plusminuschecked label { + color: black; +} + +.plusminuscheck:checked ~ label .plus { + color: green; +} + +.plusminuscheck:not(:checked) ~ label .minus { + color: red; +} + .inline-edit { input { /* important since regular spec is much stronger...*/ -- cgit v1.2.3 From 6cf6a81d38ceeaedd122ebc97f88c8fca034b57e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Mon, 28 Sep 2020 03:12:56 +0200 Subject: Add the sad sad boolean. --- static/script.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/static/script.js b/static/script.js index b94d0f2b..f5b137a3 100644 --- a/static/script.js +++ b/static/script.js @@ -728,6 +728,10 @@ window.onload = function () { })); break; + case 'boolean': + value.type = 'checkbox'; + break; + case 'period': value.type = 'text'; // TODO validate /P\d*H/ typ @@ -790,6 +794,7 @@ let all_types = [ 'utc-offset', 'cal-address', 'recur', + 'boolean', ] -- cgit v1.2.3 From 0e6050122d78ce427715deb0b08ed26fc4af1c5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Fri, 2 Oct 2020 00:35:20 +0200 Subject: Fix XML double attribute error. --- module/calp/html/vcomponent.scm | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/module/calp/html/vcomponent.scm b/module/calp/html/vcomponent.scm index 2497aa04..7a4de873 100644 --- a/module/calp/html/vcomponent.scm +++ b/module/calp/html/vcomponent.scm @@ -150,18 +150,16 @@ `(input (@ (type "checkbox") (style "display:none") (name "wholeday")))) - (input (@ ,@(when (date? start) - '((style "display:none"))) - (type "time") + (input (@ (type "time") (name "dtstart-end") - (style "grid-column:3;grid-row:2") + (style "grid-column:3;grid-row:2;" + ,(when (date? start) "display:none")) (value ,(time->string (as-time start))))) - (input (@ ,@(when (date? end) - '((style "display:none"))) - (type "time") + (input (@ (type "time") (name "dtend-time") - (style "grid-column:3;grid-row:3") + (style "grid-column:3;grid-row:3;" + ,(when (date? end) "display:none")) ,@(when end `((value ,(time->string (as-time end))))) )))) -- cgit v1.2.3 From 5c49fcb0a32e06c0e43b589f3237d6de2b831e3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Fri, 2 Oct 2020 16:49:07 +0200 Subject: Better mapping of fields. --- static/script.js | 88 +++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 58 insertions(+), 30 deletions(-) diff --git a/static/script.js b/static/script.js index f5b137a3..7fcdf48f 100644 --- a/static/script.js +++ b/static/script.js @@ -797,58 +797,86 @@ let all_types = [ 'boolean', ] - -/* TODO - Todo map for which properties are valid on which object types -*/ +let valid_fields = { + 'VCALENDAR': ['PRODID', 'VERSION', 'CALSCALE', 'METHOD'], + 'VEVENT': ['DTSTAMP', 'UID', 'DTSTART', 'CLASS', 'CREATED', + 'DESCRIPTION', 'GEO', 'LAST-MODIFIED', 'LOCATION', + 'ORGANIZER', 'PRIORITY', 'SEQUENCE', 'STATUS', + 'SUMMARY', 'TRANSP', 'URL', 'RECURRENCE-ID', + 'RRULE', 'DTEND', 'DURATION', 'ATTACH', 'ATTENDEE', + 'CATEGORIES', 'COMMENT', 'CONTACT', 'EXDATE', + 'REQUEST-STATUS', 'RELATED-TO', 'RESOURCES', 'RDATE'], + 'VTODO': ['DTSTAMP', 'UID', 'CLASS', 'COMPLETED', 'CREATED', + 'DESCRIPTION', 'DTSTART', 'GEO', 'LAST-MODIFIED', + 'LOCATION', 'ORGANIZER', 'PERCENT-COMPLETE', 'PRIORITY', + 'RECURRENCE-ID', 'SEQUENCE', 'STATUS', 'SUMMARY', 'URL', + 'RRULE', 'DUE', 'DURATION', 'ATTACH', 'ATTENDEE', 'CATEGORIES', + 'COMMENT', 'CONTACT', 'EXDATE', 'REQUEST-STATUS', 'RELATED-TO', + 'RESOURCES', 'RDATE',], + 'VJOURNAL': ['DTSTAMP', 'UID', 'CLASS', 'CREATED', 'DTSTART', 'LAST-MODIFIED', + 'ORGANIZER', 'RECURRENCE-ID', 'SEQUENCE', 'STATUS', 'SUMMARY', + 'URL', 'RRULE', 'ATTACH', 'ATTENDEE', 'CATEGORIES', 'COMMENT', + 'CONTACT', 'DESCRIPTION', 'EXDATE', 'RELATED-TO', 'RDATE', + 'REQUEST-STATUS'], + 'VFREEBUSY': ['DTSTAMP', 'UID', 'CONTACT', 'DTSTART', 'DTEND', + 'ORGANIZER', 'URL', 'ATTENDEE', 'COMMENT', 'FREEBUSY', + 'REQUEST-STATUS'], + 'VTIMEZONE': ['TZID', 'LAST-MODIFIED', 'TZURL'], + 'VALARM': ['ACTION', 'TRIGGER', 'DURATION', 'REPEAT', 'ATTACH', + 'DESCRIPTION', 'SUMMARY', 'ATTENDEE'], + 'STANDARD': ['DTSTART', 'TZOFFSETFROM', 'TZOFFSETTO', 'RRULE', + 'COMMENT', 'RDATE', 'TZNAME'], +}; + +valid_fields['DAYLIGHT'] = valid_fields['STANDARD']; let valid_input_types = { - 'CALSCALE': ['text'], - 'METHOD': ['text'], - 'PRODID': ['text'], - 'VERSION': ['text'], + 'ACTION': ['text'], // AUDIO|DISPLAY|EMAIL|*other* 'ATTACH': ['uri', 'binary'], + 'ATTENDEE': ['cal-address'], + 'CALSCALE': ['text'], 'CATEGORIES': [['text']], 'CLASS': ['text'], // PUBLIC|PRIVATE|CONFIDENTIAL|*other* 'COMMENT': ['text'], + 'COMPLETED': ['date-time'], + 'CONTACT': ['text'], + 'CREATED': ['date-time'], 'DESCRIPTION': ['text'], + 'DTEND': ['date', 'date-time'], + 'DTSTAMP': ['date-time'], + 'DTSTART': ['date', 'date-time'], + 'DUE': ['date', 'date-time'], + 'DURATION': ['duration'], + 'EXDATE': [['date', 'date-time']], + 'FREEBUSY': [['period']], 'GEO': ['float'], // pair of floats + 'LAST-MODIFIED': ['date-time'], 'LOCATION': ['text'], + 'METHOD': ['text'], + 'ORGANIZER': ['cal-address'], 'PERCENT-COMPLETE': ['integer'], // 0-100 'PRIORITY': ['integer'], // 0-9 + 'PRODID': ['text'], + 'RDATE': [['date', 'date-time', 'period']], + 'RECURRENCE-ID': ['date', 'date-time'], + 'RELATED-TO': ['text'], + 'REPEAT': ['integer'], + 'REQUEST-STATUS': ['text'], 'RESOURCES': [['text']], + 'RRULE': ['recur'], + 'SEQUENCE': ['integer'], 'STATUS': ['text'], // see 3.8.1.11 'SUMMARY': ['text'], - 'COMPLETED': ['date-time'], - 'DTEND': ['date', 'date-time'], - 'DUE': ['date', 'date-time'], - 'DTSTART': ['date', 'date-time'], - 'DURATION': ['duration'], - 'FREEBUSY': [['period']], 'TRANSP': ['text'], // OPAQUE|TRANSPARENT + 'TRIGGER': ['duration', 'date-time'], 'TZID': ['text'], 'TZNAME': ['text'], 'TZOFFSETFROM': ['utc-offset'], 'TZOFFSETTO': ['utc-offset'], 'TZURL': ['uri'], - 'ATTENDEE': ['cal-address'], - 'CONTACT': ['text'], - 'ORGANIZER': ['cal-address'], - 'RECURRENCE-ID': ['date', 'date-time'], - 'RELATED-TO': ['text'], 'URL': ['uri'], - 'EXDATE': [['date', 'date-time']], - 'RDATE': [['date', 'date-time', 'period']], - 'RRULE': ['recur'], - 'ACTION': ['text'], // AUDIO|DISPLAY|EMAIL|*other* - 'REPEAT': ['integer'], - 'TRIGGER': ['duration', 'date-time'], - 'CREATED': ['date-time'], - 'DTSTAMP': ['date-time'], - 'LAST-MODIFIED': ['date-time'], - 'SEQUENCE': ['integer'], - 'REQUEST-STATUS': ['text'] + 'VERSION': ['text'], } function close_popup(popup) { -- cgit v1.2.3 From f533f5050bb4899e18dc1656458697b1d277dd56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Fri, 2 Oct 2020 16:49:43 +0200 Subject: Binding of fields in edit tab work. --- module/calp/html/vcomponent.scm | 50 +++++++++++++++++----- static/script.js | 91 +++++++++++++++++++++-------------------- 2 files changed, 86 insertions(+), 55 deletions(-) diff --git a/module/calp/html/vcomponent.scm b/module/calp/html/vcomponent.scm index 7a4de873..0f8014db 100644 --- a/module/calp/html/vcomponent.scm +++ b/module/calp/html/vcomponent.scm @@ -56,18 +56,21 @@ ;; (format (current-error-port) "fmt-single-event: ~a~%" (prop ev 'X-HNH-FILENAME)) `(div (@ ,@(assq-merge attributes - `((class " eventtext " + `((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 "summary")) ,(prop ev 'SUMMARY)))) + `(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 "dtstart") + `(div (time (@ (class "bind dtstart") + (data-property "dtstart") (data-fmt ,(string-append "~L" start)) (datetime ,(datetime->string (as-datetime (prop ev 'DTSTART)) @@ -76,7 +79,8 @@ (as-datetime (prop ev 'DTSTART)) start)))] [(start end) - `(div (time (@ (class "dtstart") + `(div (time (@ (class "bind dtstart") + (data-property "dtstart") (data-fmt ,(string-append "~L" start)) (datetime ,(datetime->string (as-datetime (prop ev 'DTSTART)) @@ -84,23 +88,30 @@ ,(datetime->string (as-datetime (prop ev 'DTSTART)) start)) " — " - (time (@ (class "dtend") + (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 "location")) + (div (@ (class "bind location") (data-property "location")) ,(string-map (lambda (c) (if (char=? c #\,) #\newline c)) (prop ev 'LOCATION))))) ,(awhen (prop ev 'DESCRIPTION) - `(div (@ (class "description")) + `(div (@ (class "bind description") + (data-property "description")) ,(format-description ev it))) + ;; TODO add bind once I figure out how to bind lists ,(awhen (prop ev 'CATEGORIES) `(div (@ (class "categories")) ,@(map (lambda (c) @@ -111,6 +122,8 @@ c))) ,c)) it))) + + ;; TODO bind ,(awhen (prop ev 'RRULE) `(div (@ (class "rrule")) ,@(format-recurrence-rule ev))) @@ -124,11 +137,12 @@ (define*-public (fmt-for-edit ev optional: (attributes '()) key: (fmt-header list)) - `(div (@ (class " eventtext ")) + `(div (@ (class " eventtext edit-tab ")) (form (@ (class "edit-form")) (h3 (input (@ (type "text") (placeholder "Sammanfattning") (name "summary") (required) + (class "bind") (data-property "summary") (value ,(prop ev 'SUMMARY))))) ,(let ((start (prop ev 'DTSTART)) @@ -138,11 +152,15 @@ (input (@ (type "date") (name "dtstart-date") (style "grid-column:1;grid-row:2") + (class "bind") + (data-property "--dtstart-date") (value ,(date->string (as-date start))))) (input (@ (type "date") (name "dtend-date") (style "grid-column:1;grid-row:3") + (class "bind") + (data-property "--dtend-date") ,@(when end `((value ,(date->string (as-date end))))))) ,@(with-label @@ -151,13 +169,17 @@ (name "wholeday")))) (input (@ (type "time") - (name "dtstart-end") + (name "dtstart-time") + (class "bind") + (data-property "--dtstart-time") (style "grid-column:3;grid-row:2;" ,(when (date? start) "display:none")) (value ,(time->string (as-time start))))) (input (@ (type "time") (name "dtend-time") + (class "bind") + (data-property "--dtend-time") (style "grid-column:3;grid-row:3;" ,(when (date? end) "display:none")) ,@(when end `((value ,(time->string (as-time end))))) @@ -168,11 +190,13 @@ `(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))) @@ -190,6 +214,8 @@ (type "text") )))) + ;; TODO extra fields + (hr) (div (@ (class "newfield")) @@ -275,10 +301,12 @@ (div (@ (class "event-body")) ,(when (prop ev 'RRULE) `(span (@ (class "repeating")) "↺")) - (span (@ (class "summary")) + (span (@ (class "bind summary") + (data-property "summary")) ,(format-summary ev (prop ev 'SUMMARY))) ,(when (prop ev 'LOCATION) - `(span (@ (class "location")) + `(span (@ (class "bind location") + (data-property "location")) ,(string-map (lambda (c) (if (char=? c #\,) #\newline c)) (prop ev 'LOCATION))))) (div (@ (style "display:none !important;")) diff --git a/static/script.js b/static/script.js index 7fcdf48f..b703a21a 100644 --- a/static/script.js +++ b/static/script.js @@ -928,7 +928,7 @@ function toggle_popup(popup_id) { default_value - default value when creating bind_to_ical - should this property be added to the icalendar subtree? */ -function get_property(el, field, default_value, bind_to_ical=true) { +function get_property(el, field, default_value) { if (! el.properties) { el.properties = {}; } @@ -952,20 +952,6 @@ function get_property(el, field, default_value, bind_to_ical=true) { }); } - if (bind_to_ical) { - let ical_properties = el.querySelector("icalendar vevent properties"); - if (! ical_properties.querySelector(field)) { - let text = document.createElementNS(xcal, 'text'); - let element = document.createElementNS(xcal, field); - element.appendChild(text); - if (default_value) {text.innerHTML = default_value;} - - ical_properties.appendChild(element); - el.properties["_slot_" + field].push( - [text, (s, v) => s.innerHTML = v]); - } - } - return el.properties["_slot_" + field]; } @@ -1001,45 +987,61 @@ class vcomponent { and binds the value to the slot. */ function bind_properties (el, wide_event=false) { + el.properties = {} let popup = document.getElementById("popup" + el.id); // let children = el.getElementsByTagName("properties")[0].children; - let children = el.querySelector("vevent > properties").children; - for (let child of children) { - let field = child.tagName; + /* actual component (not popup) */ + for (let e of el.querySelectorAll(".bind")) { + let f = ((s, v) => s.innerHTML = v.format(s.dataset && s.dataset.fmt)); + get_property(el, e.dataset.property).push([e, f]); + } - let lst = get_property(el, field); + /* primary display tab */ - /* Bind HTML fields for this event */ - for (let s of el.getElementsByClassName(field)) { - let f = ((s, v) => s.innerHTML = v.format(s.dataset && s.dataset.fmt)); - lst.push([s, f]); + for (let e of popup.querySelectorAll(".summary-tab .bind")) { + let f = (s, v) => s.innerHTML = v.format(s.dataset && s.dataset.fmt); + get_property(el, e.dataset.property).push([e, f]); + } + + /* edit tab */ + for (let e of popup.querySelectorAll(".edit-tab .bind")) { + let p = get_property(el, e.dataset.property); + e.oninput = function () { + el.properties[e.dataset.property] = this.value; } - for (let s of popup.getElementsByClassName(field)) { - let f = ((s, v) => s.innerHTML = v.format(s.dataset && s.dataset.fmt)); - lst.push([s, f]); + let f; + switch (e.tagName) { + case 'input': + // TODO format depending on type + switch (e.type) { + case 'time': f = (s, v) => s.value = v.format("~H:~M:~S"); break; + case 'date': f = (s, v) => s.value = v.format("~Y-~m-~d"); break; + default: f = (s, v) => s.value = v; + } + p.push([e, f]) + break; + case 'textarea': + f = (s, v) => s.innerHTML = v; + p.push([e, f]) + break; + default: + alert("How did you get here??? " + e.tagName) + break; } + } - /* edit tab */ - for (let s of popup.querySelectorAll(`[name='${field}']`)) { - let f; - s.oninput = function () { - el.properties[s.name] = this.value; - } - switch (s.name) { - case 'description': - f = ((s, v) => s.innerHTML = v.format(s.dataset && s.dataset.fmt)); - lst.push([s, f]); - break; - default: - f = ((s, v) => s.value = v); - lst.push([s, f]); - break; - } - } + /* TODO propagate `--dtstart-*' to `dtstart' */ + get_property(el, 'dtstart').push( + [el, (el, v) => { el.properties['--dtstart-time'] = v; + el.properties['--dtstart-date'] = v; }]); + for (let child of el.querySelector("vevent > properties").children) { + let field = child.tagName; + + let lst = get_property(el, field); /* Bind vcomponent fields for this event */ for (let s of el.querySelectorAll(field + " > :not(parameters)")) { @@ -1055,6 +1057,7 @@ function bind_properties (el, wide_event=false) { } } + /* set up graphical display changes */ let container = el.closest(".event-container"); if (container === null) { console.log("No enclosing event container for", el); @@ -1087,7 +1090,7 @@ function bind_properties (el, wide_event=false) { el.dataset.calendar = "Unknown"; } - let calprop = get_property(el, 'calendar', el.dataset.calendar, false); + let calprop = get_property(el, 'calendar', el.dataset.calendar); const rplcs = (s, v) => { let [_, calclass] = s.classList.find(/^CAL_/); -- cgit v1.2.3 From a73c338fd87f78b872386d2b04152fa9a1ec05a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Fri, 2 Oct 2020 16:58:51 +0200 Subject: Move JS type info into own file. --- module/calp/html/view/calendar.scm | 1 + static/script.js | 98 -------------------------------------- static/types.js | 98 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+), 98 deletions(-) create mode 100644 static/types.js diff --git a/module/calp/html/view/calendar.scm b/module/calp/html/view/calendar.scm index 64986b5c..abf43118 100644 --- a/module/calp/html/view/calendar.scm +++ b/module/calp/html/view/calendar.scm @@ -104,6 +104,7 @@ ,(include-alt-css "/static/dark.css" '(title "Dark")) ,(include-alt-css "/static/light.css" '(title "Light")) + (script (@ (defer) (src "/static/types.js"))) (script (@ (defer) (src "/static/lib.js"))) (script (@ (defer) (src "/static/script.js"))) ,(calendar-styles calendars)) diff --git a/static/script.js b/static/script.js index b703a21a..cd36aafd 100644 --- a/static/script.js +++ b/static/script.js @@ -781,104 +781,6 @@ window.onload = function () { } -let all_types = [ - 'text', - 'uri', - 'binary', - 'float', - 'integer', - 'date-time', - 'date', - 'duration', - 'period', - 'utc-offset', - 'cal-address', - 'recur', - 'boolean', -] - -let valid_fields = { - 'VCALENDAR': ['PRODID', 'VERSION', 'CALSCALE', 'METHOD'], - 'VEVENT': ['DTSTAMP', 'UID', 'DTSTART', 'CLASS', 'CREATED', - 'DESCRIPTION', 'GEO', 'LAST-MODIFIED', 'LOCATION', - 'ORGANIZER', 'PRIORITY', 'SEQUENCE', 'STATUS', - 'SUMMARY', 'TRANSP', 'URL', 'RECURRENCE-ID', - 'RRULE', 'DTEND', 'DURATION', 'ATTACH', 'ATTENDEE', - 'CATEGORIES', 'COMMENT', 'CONTACT', 'EXDATE', - 'REQUEST-STATUS', 'RELATED-TO', 'RESOURCES', 'RDATE'], - 'VTODO': ['DTSTAMP', 'UID', 'CLASS', 'COMPLETED', 'CREATED', - 'DESCRIPTION', 'DTSTART', 'GEO', 'LAST-MODIFIED', - 'LOCATION', 'ORGANIZER', 'PERCENT-COMPLETE', 'PRIORITY', - 'RECURRENCE-ID', 'SEQUENCE', 'STATUS', 'SUMMARY', 'URL', - 'RRULE', 'DUE', 'DURATION', 'ATTACH', 'ATTENDEE', 'CATEGORIES', - 'COMMENT', 'CONTACT', 'EXDATE', 'REQUEST-STATUS', 'RELATED-TO', - 'RESOURCES', 'RDATE',], - 'VJOURNAL': ['DTSTAMP', 'UID', 'CLASS', 'CREATED', 'DTSTART', 'LAST-MODIFIED', - 'ORGANIZER', 'RECURRENCE-ID', 'SEQUENCE', 'STATUS', 'SUMMARY', - 'URL', 'RRULE', 'ATTACH', 'ATTENDEE', 'CATEGORIES', 'COMMENT', - 'CONTACT', 'DESCRIPTION', 'EXDATE', 'RELATED-TO', 'RDATE', - 'REQUEST-STATUS'], - 'VFREEBUSY': ['DTSTAMP', 'UID', 'CONTACT', 'DTSTART', 'DTEND', - 'ORGANIZER', 'URL', 'ATTENDEE', 'COMMENT', 'FREEBUSY', - 'REQUEST-STATUS'], - 'VTIMEZONE': ['TZID', 'LAST-MODIFIED', 'TZURL'], - 'VALARM': ['ACTION', 'TRIGGER', 'DURATION', 'REPEAT', 'ATTACH', - 'DESCRIPTION', 'SUMMARY', 'ATTENDEE'], - 'STANDARD': ['DTSTART', 'TZOFFSETFROM', 'TZOFFSETTO', 'RRULE', - 'COMMENT', 'RDATE', 'TZNAME'], -}; - -valid_fields['DAYLIGHT'] = valid_fields['STANDARD']; - - -let valid_input_types = { - 'ACTION': ['text'], // AUDIO|DISPLAY|EMAIL|*other* - 'ATTACH': ['uri', 'binary'], - 'ATTENDEE': ['cal-address'], - 'CALSCALE': ['text'], - 'CATEGORIES': [['text']], - 'CLASS': ['text'], // PUBLIC|PRIVATE|CONFIDENTIAL|*other* - 'COMMENT': ['text'], - 'COMPLETED': ['date-time'], - 'CONTACT': ['text'], - 'CREATED': ['date-time'], - 'DESCRIPTION': ['text'], - 'DTEND': ['date', 'date-time'], - 'DTSTAMP': ['date-time'], - 'DTSTART': ['date', 'date-time'], - 'DUE': ['date', 'date-time'], - 'DURATION': ['duration'], - 'EXDATE': [['date', 'date-time']], - 'FREEBUSY': [['period']], - 'GEO': ['float'], // pair of floats - 'LAST-MODIFIED': ['date-time'], - 'LOCATION': ['text'], - 'METHOD': ['text'], - 'ORGANIZER': ['cal-address'], - 'PERCENT-COMPLETE': ['integer'], // 0-100 - 'PRIORITY': ['integer'], // 0-9 - 'PRODID': ['text'], - 'RDATE': [['date', 'date-time', 'period']], - 'RECURRENCE-ID': ['date', 'date-time'], - 'RELATED-TO': ['text'], - 'REPEAT': ['integer'], - 'REQUEST-STATUS': ['text'], - 'RESOURCES': [['text']], - 'RRULE': ['recur'], - 'SEQUENCE': ['integer'], - 'STATUS': ['text'], // see 3.8.1.11 - 'SUMMARY': ['text'], - 'TRANSP': ['text'], // OPAQUE|TRANSPARENT - 'TRIGGER': ['duration', 'date-time'], - 'TZID': ['text'], - 'TZNAME': ['text'], - 'TZOFFSETFROM': ['utc-offset'], - 'TZOFFSETTO': ['utc-offset'], - 'TZURL': ['uri'], - 'URL': ['uri'], - 'VERSION': ['text'], -} - function close_popup(popup) { popup.classList.remove("visible"); } diff --git a/static/types.js b/static/types.js new file mode 100644 index 00000000..cfed8584 --- /dev/null +++ b/static/types.js @@ -0,0 +1,98 @@ + +let all_types = [ + 'text', + 'uri', + 'binary', + 'float', + 'integer', + 'date-time', + 'date', + 'duration', + 'period', + 'utc-offset', + 'cal-address', + 'recur', + 'boolean', +] + +let valid_fields = { + 'VCALENDAR': ['PRODID', 'VERSION', 'CALSCALE', 'METHOD'], + 'VEVENT': ['DTSTAMP', 'UID', 'DTSTART', 'CLASS', 'CREATED', + 'DESCRIPTION', 'GEO', 'LAST-MODIFIED', 'LOCATION', + 'ORGANIZER', 'PRIORITY', 'SEQUENCE', 'STATUS', + 'SUMMARY', 'TRANSP', 'URL', 'RECURRENCE-ID', + 'RRULE', 'DTEND', 'DURATION', 'ATTACH', 'ATTENDEE', + 'CATEGORIES', 'COMMENT', 'CONTACT', 'EXDATE', + 'REQUEST-STATUS', 'RELATED-TO', 'RESOURCES', 'RDATE'], + 'VTODO': ['DTSTAMP', 'UID', 'CLASS', 'COMPLETED', 'CREATED', + 'DESCRIPTION', 'DTSTART', 'GEO', 'LAST-MODIFIED', + 'LOCATION', 'ORGANIZER', 'PERCENT-COMPLETE', 'PRIORITY', + 'RECURRENCE-ID', 'SEQUENCE', 'STATUS', 'SUMMARY', 'URL', + 'RRULE', 'DUE', 'DURATION', 'ATTACH', 'ATTENDEE', 'CATEGORIES', + 'COMMENT', 'CONTACT', 'EXDATE', 'REQUEST-STATUS', 'RELATED-TO', + 'RESOURCES', 'RDATE',], + 'VJOURNAL': ['DTSTAMP', 'UID', 'CLASS', 'CREATED', 'DTSTART', 'LAST-MODIFIED', + 'ORGANIZER', 'RECURRENCE-ID', 'SEQUENCE', 'STATUS', 'SUMMARY', + 'URL', 'RRULE', 'ATTACH', 'ATTENDEE', 'CATEGORIES', 'COMMENT', + 'CONTACT', 'DESCRIPTION', 'EXDATE', 'RELATED-TO', 'RDATE', + 'REQUEST-STATUS'], + 'VFREEBUSY': ['DTSTAMP', 'UID', 'CONTACT', 'DTSTART', 'DTEND', + 'ORGANIZER', 'URL', 'ATTENDEE', 'COMMENT', 'FREEBUSY', + 'REQUEST-STATUS'], + 'VTIMEZONE': ['TZID', 'LAST-MODIFIED', 'TZURL'], + 'VALARM': ['ACTION', 'TRIGGER', 'DURATION', 'REPEAT', 'ATTACH', + 'DESCRIPTION', 'SUMMARY', 'ATTENDEE'], + 'STANDARD': ['DTSTART', 'TZOFFSETFROM', 'TZOFFSETTO', 'RRULE', + 'COMMENT', 'RDATE', 'TZNAME'], +}; + +valid_fields['DAYLIGHT'] = valid_fields['STANDARD']; + + +let valid_input_types = { + 'ACTION': ['text'], // AUDIO|DISPLAY|EMAIL|*other* + 'ATTACH': ['uri', 'binary'], + 'ATTENDEE': ['cal-address'], + 'CALSCALE': ['text'], + 'CATEGORIES': [['text']], + 'CLASS': ['text'], // PUBLIC|PRIVATE|CONFIDENTIAL|*other* + 'COMMENT': ['text'], + 'COMPLETED': ['date-time'], + 'CONTACT': ['text'], + 'CREATED': ['date-time'], + 'DESCRIPTION': ['text'], + 'DTEND': ['date', 'date-time'], + 'DTSTAMP': ['date-time'], + 'DTSTART': ['date', 'date-time'], + 'DUE': ['date', 'date-time'], + 'DURATION': ['duration'], + 'EXDATE': [['date', 'date-time']], + 'FREEBUSY': [['period']], + 'GEO': ['float'], // pair of floats + 'LAST-MODIFIED': ['date-time'], + 'LOCATION': ['text'], + 'METHOD': ['text'], + 'ORGANIZER': ['cal-address'], + 'PERCENT-COMPLETE': ['integer'], // 0-100 + 'PRIORITY': ['integer'], // 0-9 + 'PRODID': ['text'], + 'RDATE': [['date', 'date-time', 'period']], + 'RECURRENCE-ID': ['date', 'date-time'], + 'RELATED-TO': ['text'], + 'REPEAT': ['integer'], + 'REQUEST-STATUS': ['text'], + 'RESOURCES': [['text']], + 'RRULE': ['recur'], + 'SEQUENCE': ['integer'], + 'STATUS': ['text'], // see 3.8.1.11 + 'SUMMARY': ['text'], + 'TRANSP': ['text'], // OPAQUE|TRANSPARENT + 'TRIGGER': ['duration', 'date-time'], + 'TZID': ['text'], + 'TZNAME': ['text'], + 'TZOFFSETFROM': ['utc-offset'], + 'TZOFFSETTO': ['utc-offset'], + 'TZURL': ['uri'], + 'URL': ['uri'], + 'VERSION': ['text'], +} -- cgit v1.2.3 From 1e11a54edb2f922d5266fedfb8345e9ed7a30729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Sun, 4 Oct 2020 03:33:07 +0200 Subject: Fix datetime input for events. --- static/script.js | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/static/script.js b/static/script.js index cd36aafd..1544f071 100644 --- a/static/script.js +++ b/static/script.js @@ -916,10 +916,10 @@ function bind_properties (el, wide_event=false) { let f; switch (e.tagName) { case 'input': - // TODO format depending on type switch (e.type) { case 'time': f = (s, v) => s.value = v.format("~H:~M:~S"); break; case 'date': f = (s, v) => s.value = v.format("~Y-~m-~d"); break; + // TODO remaining types cases default: f = (s, v) => s.value = v; } p.push([e, f]) @@ -935,10 +935,37 @@ function bind_properties (el, wide_event=false) { } - /* TODO propagate `--dtstart-*' to `dtstart' */ - get_property(el, 'dtstart').push( - [el, (el, v) => { el.properties['--dtstart-time'] = v; - el.properties['--dtstart-date'] = v; }]); + for (let field of ['dtstart', 'dtend']) { + get_property(el, `--{field}-time`).push( + [el, (el, v) => { let date = el.properties.dtstart; + let [h,m,s] = v.split(':') + date.setHours(Number(h)); + date.setMinutes(Number(m)); + el.properties[field] = date; }]) + get_property(el, `--{field}-date`).push( + [el, (el, v) => { let date = el.properties.dtstart; + let [y,m,d] = v.split('-') + date.setYear(Number(y) - 1900); + date.setMinutes(Number(m) - 1); + el.properties[field] = date; }]) + + + /* Manual fetch of the fields instead of the general method, + to avoid an infinite loop of dtstart setting --dtstart-time, + and vice versa. + NOTE if many more fields require special treatment then a + general solution is required. + */ + get_property(el, field).push( + [el, (el, v) => { popup + .querySelector(`.edit-tab input[name='{field}-time']`) + .value = v.format("~H:~M:~S"); + popup + .querySelector(`.edit-tab input[name='{field}-date']`) + .value = v.format("~Y-~m-~d"); + }]); + } + for (let child of el.querySelector("vevent > properties").children) { let field = child.tagName; -- cgit v1.2.3 From 82433fdfae33be439ddb764d1c5739b34d7d0343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Mon, 5 Oct 2020 01:32:37 +0200 Subject: $ needed for backtick strings. --- static/script.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/static/script.js b/static/script.js index 1544f071..1a893e56 100644 --- a/static/script.js +++ b/static/script.js @@ -936,13 +936,13 @@ function bind_properties (el, wide_event=false) { for (let field of ['dtstart', 'dtend']) { - get_property(el, `--{field}-time`).push( + get_property(el, `--${field}-time`).push( [el, (el, v) => { let date = el.properties.dtstart; let [h,m,s] = v.split(':') date.setHours(Number(h)); date.setMinutes(Number(m)); el.properties[field] = date; }]) - get_property(el, `--{field}-date`).push( + get_property(el, `--${field}-date`).push( [el, (el, v) => { let date = el.properties.dtstart; let [y,m,d] = v.split('-') date.setYear(Number(y) - 1900); @@ -958,10 +958,10 @@ function bind_properties (el, wide_event=false) { */ get_property(el, field).push( [el, (el, v) => { popup - .querySelector(`.edit-tab input[name='{field}-time']`) + .querySelector(`.edit-tab input[name='${field}-time']`) .value = v.format("~H:~M:~S"); popup - .querySelector(`.edit-tab input[name='{field}-date']`) + .querySelector(`.edit-tab input[name='${field}-date']`) .value = v.format("~Y-~m-~d"); }]); } @@ -973,7 +973,7 @@ function bind_properties (el, wide_event=false) { let lst = get_property(el, field); /* Bind vcomponent fields for this event */ - for (let s of el.querySelectorAll(field + " > :not(parameters)")) { + for (let s of el.querySelectorAll(`${field} > :not(parameters)`)) { switch (s.tagName) { case 'date': lst.push([s, (s, v) => s.innerHTML = v.format("~Y-~m-~d")]); break; -- cgit v1.2.3 From c8a09ca13b97eddc5034f3e0454207d700f0105b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Mon, 5 Oct 2020 01:44:42 +0200 Subject: Edit tab submit now bound. --- static/script.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/static/script.js b/static/script.js index 1a893e56..32e19cc4 100644 --- a/static/script.js +++ b/static/script.js @@ -575,6 +575,14 @@ window.onload = function () { */ el.parentElement.removeAttribute("href"); + /* TODO this doesn't yet apply to newly created events */ + let popup = document.getElementById("popup" + el.id); + let form = popup.getElementsByClassName("edit-form")[0]; + form.onsubmit = function () { + create_event(el); + return false; /* stop default */ + } + /* Bind all vcomponent properties into javascript. */ if (el.closest(".longevents")) { bind_properties(el, true); -- cgit v1.2.3 From 04348715ae39c333c267cea768878319076c3f7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Mon, 5 Oct 2020 01:44:58 +0200 Subject: Setting DTEND now works. --- static/script.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/script.js b/static/script.js index 32e19cc4..fa9ef210 100644 --- a/static/script.js +++ b/static/script.js @@ -945,13 +945,13 @@ function bind_properties (el, wide_event=false) { for (let field of ['dtstart', 'dtend']) { get_property(el, `--${field}-time`).push( - [el, (el, v) => { let date = el.properties.dtstart; + [el, (el, v) => { let date = el.properties[field]; let [h,m,s] = v.split(':') date.setHours(Number(h)); date.setMinutes(Number(m)); el.properties[field] = date; }]) get_property(el, `--${field}-date`).push( - [el, (el, v) => { let date = el.properties.dtstart; + [el, (el, v) => { let date = el.properties[field]; let [y,m,d] = v.split('-') date.setYear(Number(y) - 1900); date.setMinutes(Number(m) - 1); -- cgit v1.2.3 From 6e240ba91752990c6cb087e576432ce9b6a8ad44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Mon, 5 Oct 2020 02:17:09 +0200 Subject: Simplify /insert endpoint. --- module/calp/server/routes.scm | 66 ++++++++++++++++++------------------------- module/vcomponent/base.scm | 8 ------ 2 files changed, 27 insertions(+), 47 deletions(-) diff --git a/module/calp/server/routes.scm b/module/calp/server/routes.scm index 184b4481..276513f5 100644 --- a/module/calp/server/routes.scm +++ b/module/calp/server/routes.scm @@ -218,55 +218,43 @@ [(get-event-by-uid global-event-object (prop event 'UID)) => (lambda (old-event) - ;; procedure to run after save. - ;; used as hook to remove old event from disk below - (define after-save (const #f)) - - (if (eq? calendar (parent old-event)) - (begin (vcomponent-update! old-event event) - ;; for save below - (set! event old-event)) - - ;; change calendar - (begin - - (format (current-error-port) - "Calendar change~%") - - ;; remove from runtime - ((@ (vcomponent instance methods) remove-event) - global-event-object old-event) - - ;; Actually puring the old event should be safe, - ;; since we first make sure we write the new event to disk. - ;; Currently the whole transaction isn't atomic, so a duplicate - ;; event can still be created. - (set! after-save - ;; remove from disk - (lambda () - (format (current-error-port) - "Unlinking old event from ~a~%" - (prop old-event '-X-HNH-FILENAME)) - ((@ (vcomponent vdir save-delete) remove-event) old-event))) - - (parameterize ((warnings-are-errors #t)) - (catch 'warning - (lambda () (add-event global-event-object calendar event)) - (lambda (err fmt args) - (return (build-response code: 400) - (format #f "~?~%" fmt args))))))) + ;; remove old instance of event from runtime + ((@ (vcomponent instance methods) remove-event) + global-event-object old-event) + + ;; Add new event to runtime, + ;; MUST be done after since the two events SHOULD share UID. + (parameterize ((warnings-are-errors #t)) + (catch 'warning + (lambda () (add-event global-event-object calendar event)) + (lambda (err fmt args) + (return (build-response code: 400) + (format #f "~?~%" fmt args))))) (set! (prop event 'LAST-MODIFIED) (current-datetime)) - ;; NOTE Posibly defer save to a later point. ;; That would allow better asyncronous preformance. + + ;; save-event sets -X-HNH-FILENAME from the UID. This is fine + ;; since the two events are guaranteed to have the same UID. (unless ((@ (vcomponent vdir save-delete) save-event) event) (return (build-response code: 500) "Saving event to disk failed.")) - (after-save) + + (unless (eq? calendar (parent old-event)) + ;; change to a new calendar + (format (current-error-port) + "Unlinking old event from ~a~%" + (prop old-event '-X-HNH-FILENAME)) + ;; NOTE that this may fail, leading to a duplicate event being + ;; created (since we save beforehand). This is just a minor problem + ;; which either a better atomic model, or a propper error + ;; recovery log would solve. + ((@ (vcomponent vdir save-delete) remove-event) old-event)) + (format (current-error-port) "Event updated ~a~%" (prop event 'UID)))] diff --git a/module/vcomponent/base.scm b/module/vcomponent/base.scm index ae10fe01..34d4416b 100644 --- a/module/vcomponent/base.scm +++ b/module/vcomponent/base.scm @@ -169,14 +169,6 @@ (copy-vline value)))) (get-component-properties component))))) -;; updates target with all fields from source. -;; fields in target but not in source left unchanged. -;; parent and children unchanged -(define-public (vcomponent-update! target source) - (for key in (property-keys source) - (set! (prop* target key) - (prop* source key)))) - (define-public (extract field) (lambda (e) (prop e field))) -- cgit v1.2.3 From 61ebd0064fa6660667ca42ea54a169b629344034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Mon, 5 Oct 2020 02:24:04 +0200 Subject: Location and Property can now be added. --- static/script.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/static/script.js b/static/script.js index fa9ef210..f116deaf 100644 --- a/static/script.js +++ b/static/script.js @@ -994,6 +994,29 @@ function bind_properties (el, wide_event=false) { } } + /* Dynamicly add or remove the and elements + from the list. + + TODO generalize this to all fields, /especially/ those which are + dynamicly added. + */ + for (let field of ['location', 'description']) { + get_property(el, field).push( + [el.querySelector('vevent > properties'), + (s, v) => { + let slot = s.querySelector(field); + if (v === '' && slot) { + slot.remove(); + } else { + if (! slot) { + /* finns det verkligen inget bättre sätt... */ + s.innerHTML += `<${field}>`; + } + s.querySelector(`${field} > text`).innerHTML = v; + } + }]); + } + /* set up graphical display changes */ let container = el.closest(".event-container"); if (container === null) { -- cgit v1.2.3 From c0fe5487f4c46ac05a5a2bda40e0d8150bf38690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Mon, 5 Oct 2020 02:37:17 +0200 Subject: Bind new form for cloned events. --- static/script.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/static/script.js b/static/script.js index f116deaf..d218bf9f 100644 --- a/static/script.js +++ b/static/script.js @@ -64,6 +64,11 @@ class EventCreator { let event = document.getElementById("event-template").firstChild.cloneNode(true); let popup = document.getElementById("popup-template").firstChild.cloneNode(true); + popup.getElementsByClassName("edit-form")[0].onsubmit = function () { + create_event(event); + return false; /* stop default */ + } + let id = gensym ("__js_event"); // TODO remove button? @@ -577,8 +582,7 @@ window.onload = function () { /* TODO this doesn't yet apply to newly created events */ let popup = document.getElementById("popup" + el.id); - let form = popup.getElementsByClassName("edit-form")[0]; - form.onsubmit = function () { + popup.getElementsByClassName("edit-form")[0].onsubmit = function () { create_event(el); return false; /* stop default */ } -- cgit v1.2.3 From 7fd091319ee3cca1abea5e7bfcd3e6271f452015 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Mon, 5 Oct 2020 02:44:28 +0200 Subject: Remove old edit mode. --- module/calp/html/vcomponent.scm | 1 + static/script.js | 156 +++------------------------------------- 2 files changed, 12 insertions(+), 145 deletions(-) diff --git a/module/calp/html/vcomponent.scm b/module/calp/html/vcomponent.scm index 0f8014db..7414654b 100644 --- a/module/calp/html/vcomponent.scm +++ b/module/calp/html/vcomponent.scm @@ -139,6 +139,7 @@ key: (fmt-header list)) `(div (@ (class " eventtext edit-tab ")) (form (@ (class "edit-form")) + (div (@ (class "dropdown-goes-here"))) (h3 (input (@ (type "text") (placeholder "Sammanfattning") (name "summary") (required) diff --git a/static/script.js b/static/script.js index d218bf9f..dce70ee5 100644 --- a/static/script.js +++ b/static/script.js @@ -161,7 +161,7 @@ class EventCreator { bind_properties(event, wide_element); /* requires that dtstart and dtend properties are initialized */ - place_in_edit_mode(event); + // place_in_edit_mode(event); /* ---------------------------------------- */ @@ -222,6 +222,8 @@ class EventCreator { e.style.pointerEvents = ""; } + place_in_edit_mode(that.event); + let localevent = that.event; that.event = null; @@ -350,126 +352,15 @@ async function create_event (event) { toggle_popup("popup" + event.id); } -function place_in_edit_mode (event) { - let popup = document.getElementById("popup" + event.id) - function replace_with_time_input(fieldname, event) { - let field = popup.getElementsByClassName(fieldname)[0]; - - let dt = new Date(field.dateTime); - - let dateinput = makeElement ('input', { - type: 'date', - required: true, - value: dt.format("~Y-~m-~d"), - - onchange: function (e) { - /* Only update datetime when the input is filled out */ - if (! this.value) return; - let [year, month, day] = this.value.split("-").map(Number); - /* retain the hour and second information */ - let d = copyDate(event.properties[fieldname]); - d.setYear(year); - d.setMonth(month - 1); - d.setDate(day); - event.properties[fieldname] = d; - } - }); - - let timeinput = makeElement ('input', { - type: "time", - required: true, - value: dt.format("~H:~M"), - - onchange: function (e) { - /* Only update datetime when the input is filled out */ - if (! this.value) return; - let [hour, minute] = this.value.split(":").map(Number); - /* retain the year, month, and day information */ - let d = copyDate(event.properties[fieldname]); - d.setHours(hour); - d.setMinutes(minute); - event.properties[fieldname] = d; - } - }); - let slot = get_property(event, fieldname); - let idx = slot.findIndex(e => e[0] === field); - slot.splice(idx, 1, [timeinput, (s, v) => s.value = v.format("~H:~M")]) - slot.splice(idx, 0, [dateinput, (s, v) => s.value = v.format("~Y-~m-~d")]) - - field.innerHTML = ''; - field.appendChild(dateinput); - field.appendChild(timeinput); - // field.replaceWith(timeinput); - - } - - /* TODO ensure dtstart < dtend */ - replace_with_time_input("dtstart", event); - replace_with_time_input("dtend", event); - - /* ---------------------------------------- */ - - let summary = popup.getElementsByClassName("summary")[0]; - let input = makeElement('input', { - name: "summary", - value: summary.innerText, - placeholder: "Sammanfattning", - required: true, - }); - - input.oninput = function () { - event.properties["summary"] = this.value; - } - - let slot = get_property(event, "summary"); - let idx = slot.findIndex(e => e[0] === summary); - slot.splice(idx, 1, [input, (s, v) => s.value = v]) - - summary.replaceWith(input); - - /* ---------------------------------------- */ - - /* TODO add elements if the arent't already there - * Almost all should be direct children of '.event-body' (or - * '.eventtext'?). - * Biggest problem is generated fields relative order. - */ - { - let descs = popup.getElementsByClassName("description"); - let description; - if (descs.length === 1) { - description = descs[0]; - } else { - let fields = popup.getElementsByClassName("fields")[0] - description = makeElement('span', { - class: 'description', - }); - fields.appendChild(description); - let slot = get_property(event, "description"); - slot.push([description, (s, v) => s.innerHTML = v]); - } - - let textarea = makeElement('textarea', { - name: "description", - placeholder: "Description (optional)", - innerHTML: description.innerText, - required: false, - }); - textarea.oninput = function () { - event.properties["description"] = this.value; - } - - let slot = get_property(event, "description"); - let idx = slot.findIndex(e => e[0] === description); - slot.splice(idx, 1, [input, (s, v) => s.innerHTML = v]) +/* This incarnation of this function only adds the calendar switcher dropdown. + All events are already editable by switching to that tab. - description.replaceWith(textarea); - } - - /* ---------------------------------------- */ - - let evtext = popup.getElementsByClassName('eventtext')[0] + 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_/); @@ -494,32 +385,7 @@ function place_in_edit_mode (event) { calendar_dropdown.onchange = function () { event.properties.calendar = this.value; } - evtext.prepend(calendar_dropdown); - - /* ---------------------------------------- */ - - let submit = makeElement( 'input', { - type: 'submit', - value: 'Skapa event', - }); - - let article = popup.getElementsByClassName("eventtext")[0]; - article.appendChild(submit); - - - let wrappingForm = makeElement('form', { - onsubmit: function (e) { - create_event(event); - return false; - }}); - article.replaceWith(wrappingForm); - wrappingForm.appendChild(article); - - /* this is for existing events. - * Newly created events aren't in the DOM tree yet, and can - * therefore not yet be focused */ - input.focus(); - + container.appendChild(calendar_dropdown); } window.onload = function () { -- cgit v1.2.3 From 8376c96825a91009a0aa3993c9a64de1e4555a66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Mon, 5 Oct 2020 03:07:49 +0200 Subject: whitespace fixes. --- static/script.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/static/script.js b/static/script.js index dce70ee5..894ebb20 100644 --- a/static/script.js +++ b/static/script.js @@ -364,19 +364,19 @@ function place_in_edit_mode (event) { 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; - } + 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 -- cgit v1.2.3 From 31dbf6755e99205f35d2802189f67cf5464dbc13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Sat, 10 Oct 2020 16:11:44 +0200 Subject: HTML Select summary when creating new event. --- static/script.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/static/script.js b/static/script.js index 894ebb20..5ed9c1e5 100644 --- a/static/script.js +++ b/static/script.js @@ -386,6 +386,11 @@ function place_in_edit_mode (event) { 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(); } window.onload = function () { -- cgit v1.2.3 From d1d3093d3cae62e74e6be42de03cdd66230a3cf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Mon, 12 Oct 2020 17:40:06 +0200 Subject: HTML fix date input. --- static/script.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/static/script.js b/static/script.js index 5ed9c1e5..af80c15e 100644 --- a/static/script.js +++ b/static/script.js @@ -800,7 +800,7 @@ function bind_properties (el, wide_event=false) { switch (e.tagName) { case 'input': switch (e.type) { - case 'time': f = (s, v) => s.value = v.format("~H:~M:~S"); break; + case 'time': f = (s, v) => s.value = v.format("~H:~M"); break; case 'date': f = (s, v) => s.value = v.format("~Y-~m-~d"); break; // TODO remaining types cases default: f = (s, v) => s.value = v; @@ -819,17 +819,22 @@ function bind_properties (el, wide_event=false) { for (let field of ['dtstart', 'dtend']) { + get_property(el, `--${field}-time`).push( [el, (el, v) => { let date = el.properties[field]; + if (v == '') return; let [h,m,s] = v.split(':') date.setHours(Number(h)); date.setMinutes(Number(m)); + date.setSeconds(0); el.properties[field] = date; }]) get_property(el, `--${field}-date`).push( [el, (el, v) => { let date = el.properties[field]; + if (v == '') return; let [y,m,d] = v.split('-') - date.setYear(Number(y) - 1900); - date.setMinutes(Number(m) - 1); + date.setYear(Number(y)/* - 1900*/); + date.setMonth(Number(m) - 1); + date.setDate(d); el.properties[field] = date; }]) @@ -842,7 +847,7 @@ function bind_properties (el, wide_event=false) { get_property(el, field).push( [el, (el, v) => { popup .querySelector(`.edit-tab input[name='${field}-time']`) - .value = v.format("~H:~M:~S"); + .value = v.format("~H:~M"); popup .querySelector(`.edit-tab input[name='${field}-date']`) .value = v.format("~Y-~m-~d"); @@ -861,7 +866,7 @@ function bind_properties (el, wide_event=false) { case 'date': lst.push([s, (s, v) => s.innerHTML = v.format("~Y-~m-~d")]); break; case 'date-time': - lst.push([s, (s, v) => s.innerHTML = v.format("~Y-~m-~dT~H:~M:~S~Z")]); break; + lst.push([s, (s, v) => s.innerHTML = v.format("~Y-~m-~dT~H:~M:00~Z")]); break; default: lst.push([s, (s, v) => s.innerHTML = v]); } -- cgit v1.2.3 From c6ecb5325a8afdbb39b0bc90e85fdec04c61330d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Tue, 13 Oct 2020 16:40:51 +0200 Subject: Config loading now in 'sandbox'. --- config.scm | 13 ++--------- module/calp/main.scm | 64 ++++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 54 insertions(+), 23 deletions(-) diff --git a/config.scm b/config.scm index 4092ecb3..cb5779f4 100644 --- a/config.scm +++ b/config.scm @@ -2,22 +2,13 @@ ;;; Currently loaded by main, and requires that `calendar-files` ;;; is set to a list of files (or directories). -(use-modules (vcomponent)) - -(use-modules (srfi srfi-88) - (ice-9 regex) - ;; (ice-9 rdelim) +(use-modules (ice-9 regex) (sxml simple) - (glob) - - (calp util config) - - (datetime) ;; TODO this module introduces description-filter. It should be ;; possible to use set-config! before the declaration point is ;; known. But I currently get a config error. - (vcomponent datetime output) + ;; (vcomponent datetime output) ) (set-config! 'calendar-files (glob "~/.local/var/cal/*")) diff --git a/module/calp/main.scm b/module/calp/main.scm index 407f7b81..2eb1ee05 100644 --- a/module/calp/main.scm +++ b/module/calp/main.scm @@ -15,6 +15,8 @@ :use-module (ice-9 getopt-long) :use-module (ice-9 regex) :use-module ((ice-9 popen) :select (open-input-pipe)) + :use-module ((ice-9 sandbox) :select + (make-sandbox-module all-pure-and-impure-bindings)) :use-module (statprof) :use-module (calp repl) @@ -99,6 +101,11 @@ (if (null? a) b a)) +(define (bindings-for module-name) + ;; Wrapping list so we can later export sub-modules. + (list (cons module-name + (module-map (lambda (a . _) a) + (resolve-interface module-name))))) (define (wrapped-main args) (define opts (getopt-long args (getopt-opt options) #:stop-at-first-non-option #t)) @@ -106,6 +113,20 @@ (define repl (option-ref opts 'repl #f)) (define altconfig (option-ref opts 'config #f)) + (define config-file + (cond [altconfig + (if (file-exists? altconfig) + altconfig + (throw 'option-error + "Configuration file ~a missing" altconfig))] + ;; altconfig could be placed in the list below. But I want to raise an error + ;; if an explicitly given config is missing. + [(find file-exists? + (list + (path-append (xdg-config-home) "/calp/config.scm") + (path-append (xdg-sysconfdir) "/calp/config.scm"))) + => identity])) + (when stprof (statprof-start)) (cond [(eqv? #t repl) (repl-start (format #f "~a/calp-~a" @@ -113,18 +134,37 @@ (getpid)))] [repl => repl-start]) - (if altconfig - (begin - (if (file-exists? altconfig) - (primitive-load altconfig) - (throw 'option-error "Configuration file ~a missing" altconfig))) - ;; if not altconfig, then regular config - - (awhen (find file-exists? - (list - (path-append (xdg-config-home) "/calp/config.scm") - (path-append (xdg-sysconfdir) "/calp/config.scm"))) - (primitive-load it))) + + ;; load config + (catch #t + (lambda () + (eval + `(begin + (use-modules (srfi srfi-1) + (srfi srfi-88) + (datetime) + (vcomponent) + (calp util config) + (glob)) + ,@(with-input-from-file config-file + (lambda () + (let loop ((done '())) + (let ((form (read))) + (if (eof-object? form) + (reverse done) + (loop (cons form done)))))))) + (make-sandbox-module + `(((guile) use-modules) + ,@all-pure-and-impure-bindings + )) + )) + (lambda args + (format (current-error-port) + "Failed loading config file ~a~%~s~%" + config-file + args + ))) + ;; NOTE this doesn't stop at first non-option, meaning that -o flags -- cgit v1.2.3 From 5714f53e6c038598c7ae17cfc9b359fff2fa698f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Thu, 15 Oct 2020 00:12:04 +0200 Subject: Clarify use of sandbox. --- module/calp/main.scm | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/module/calp/main.scm b/module/calp/main.scm index 2eb1ee05..92f33280 100644 --- a/module/calp/main.scm +++ b/module/calp/main.scm @@ -135,7 +135,10 @@ [repl => repl-start]) - ;; load config + ;; Load config + ;; Sandbox and "stuff" not for security from the user. The config script is + ;; assumed to be "safe". Instead it's so we can control the environment in + ;; which it is executed. (catch #t (lambda () (eval -- cgit v1.2.3 From 99120bc1b7920261e1020d501de139c71a381492 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Thu, 15 Oct 2020 00:12:20 +0200 Subject: Datetime bindings for search. --- module/calp/main.scm | 6 ------ module/vcomponent/search.scm | 9 ++++++++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/module/calp/main.scm b/module/calp/main.scm index 92f33280..e296632a 100644 --- a/module/calp/main.scm +++ b/module/calp/main.scm @@ -101,12 +101,6 @@ (if (null? a) b a)) -(define (bindings-for module-name) - ;; Wrapping list so we can later export sub-modules. - (list (cons module-name - (module-map (lambda (a . _) a) - (resolve-interface module-name))))) - (define (wrapped-main args) (define opts (getopt-long args (getopt-opt options) #:stop-at-first-non-option #t)) (define stprof (option-ref opts 'statprof #f)) diff --git a/module/vcomponent/search.scm b/module/vcomponent/search.scm index 7d039a24..fef0b100 100644 --- a/module/vcomponent/search.scm +++ b/module/vcomponent/search.scm @@ -52,6 +52,13 @@ (define-public (prepare-string str) (call-with-input-string (close-parenthese str) read)) +;; TODO place this in a proper module +(define (bindings-for module-name) + ;; Wrapping list so we can later export sub-modules. + (list (cons module-name + (module-map (lambda (a . _) a) + (resolve-interface module-name))))) + ;; Evaluates the given expression in a sandbox. ;; NOTE Should maybe be merged inte prepare-query. The argument against is that ;; eval-in-sandbox is possibly slow, and that would prevent easy caching by the @@ -65,7 +72,7 @@ `( ((vcomponent base) prop param children type) ((ice-9 regex) string-match) - ;; TODO datetime + ,@(bindings-for '(datetime)) ,@all-pure-bindings) ))) -- cgit v1.2.3 From 8cced0532ab69a2346654540a4d01bc64392c359 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Thu, 15 Oct 2020 00:30:15 +0200 Subject: Fix error propagation for some search queries. --- module/vcomponent/search.scm | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/module/vcomponent/search.scm b/module/vcomponent/search.scm index fef0b100..27483720 100644 --- a/module/vcomponent/search.scm +++ b/module/vcomponent/search.scm @@ -162,8 +162,11 @@ (set-max-page! paginator (max page (get-max-page paginator))) result)))) (lambda (err proc fmt args data) - ;; TODO ensure the error actually is index out of range. - ;; (format (current-error-port) "~?~%" fmt args) + ;; NOTE This is mostly a hack to see that we + ;; actually check for the correct error. + (unless (string=? fmt "beyond end of stream") + (scm-error err proc fmt args data)) + (set-max-page! paginator (get-max-page paginator)) (set-true-max-page! paginator) (throw 'max-page (get-max-page paginator)) -- cgit v1.2.3 From 5635b5993744731f9456c671490a4702c45cd5d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Thu, 15 Oct 2020 00:31:51 +0200 Subject: Add rudementary server logging. --- module/web/http/make-routes.scm | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/module/web/http/make-routes.scm b/module/web/http/make-routes.scm index ab5f88a7..4fb5397a 100644 --- a/module/web/http/make-routes.scm +++ b/module/web/http/make-routes.scm @@ -71,6 +71,8 @@ (r:port ((@ (web request) request-port) request))) (let ((r:scheme ((@ (web uri) uri-scheme) r:uri)) (r:userinfo ((@ (web uri) uri-userinfo) r:uri)) + ;; TODO can sometimes be a pair of host and port + ;; '("localhost" . 8080). It shouldn't... (r:host (or ((@ (web uri) uri-host) r:uri) ((@ (web request) request-host) request))) @@ -80,6 +82,11 @@ (r:path ((@ (web uri) uri-path) r:uri)) (r:query ((@ (web uri) uri-query) r:uri)) (r:fragment ((@ (web uri) uri-fragment) r:uri))) + ;; TODO propper logging + (display (format #f "[~a] ~a ~a/~a?~a~%" + (datetime->string (current-datetime)) + r:method r:host r:path (or r:query "")) + (current-error-port)) (call-with-values (lambda () ((@ (ice-9 control) call/ec) -- cgit v1.2.3 From 5986d8022fe2a58df6bc7054ef9499961fb776b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Fri, 16 Oct 2020 21:42:41 +0200 Subject: HTML add toggle for whole-day. --- module/vcomponent/xcal/parse.scm | 11 ++++++++++- static/script.js | 41 +++++++++++++++++++++++++++++++--------- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/module/vcomponent/xcal/parse.scm b/module/vcomponent/xcal/parse.scm index 17c684fc..c97bc492 100644 --- a/module/vcomponent/xcal/parse.scm +++ b/module/vcomponent/xcal/parse.scm @@ -25,7 +25,10 @@ ;; TODO possibly trim whitespace on text fields [(cal-address uri text unknown) (car value)] - [(date) (parse-iso-date (car value))] + [(date) + ;; TODO this is correct, but ensure remaining types + (hashq-set! props 'VALUE "DATE") + (parse-iso-date (car value))] [(date-time) (parse-iso-datetime (car value))] @@ -108,6 +111,12 @@ data '(AUDIO DISPLAY EMAIL NONE))) [else data])) +;; Note +;; This doesn't verify the inter-field validity of the object, +;; meaning that value(DTSTART) == DATE and value(DTEND) == DATE-TIME +;; are possibilities, which other parts of the code will crash on. +;; TODO +;; since we are feeding user input into this it really should be fixed. (define-public (sxcal->vcomponent sxcal) (define type (symbol-upcase (car sxcal))) (define component (make-vcomponent type)) diff --git a/static/script.js b/static/script.js index af80c15e..636bf086 100644 --- a/static/script.js +++ b/static/script.js @@ -817,6 +817,21 @@ function bind_properties (el, wide_event=false) { } } + /* checkbox for whole day */ + 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 d = el.properties[f]; + if (! d) continue; /* dtend optional */ + d.isWholeDay = wholeday.checked; + el.properties[f] = d; + } + }); + for (let field of ['dtstart', 'dtend']) { @@ -855,21 +870,29 @@ function bind_properties (el, wide_event=false) { } + /* icalendar properties */ for (let child of el.querySelector("vevent > properties").children) { - let field = child.tagName; + /* child ≡ ... */ + let field = child.tagName; let lst = get_property(el, field); /* Bind vcomponent fields for this event */ for (let s of el.querySelectorAll(`${field} > :not(parameters)`)) { - switch (s.tagName) { - case 'date': - lst.push([s, (s, v) => s.innerHTML = v.format("~Y-~m-~d")]); break; - case 'date-time': - lst.push([s, (s, v) => s.innerHTML = v.format("~Y-~m-~dT~H:~M:00~Z")]); break; - default: - lst.push([s, (s, v) => s.innerHTML = v]); - } + lst.push([s, (s, v) => { + if (v instanceof Date) { + if (v.isWholeDay) { + let str = v.format('~Y-~m-~d'); + child.innerHTML = `${str}`; + } else { + let str = v.format('~Y-~m-~dT~H:~M:00~Z'); + child.innerHTML = `${str}`; + } + } else { + /* assume that type already is correct */ + s.innerHTML = v; + } + }]); el.properties["_value_" + field] = s.innerHTML; } } -- cgit v1.2.3 From aafc838c0a1e4c08636c950d0d4fa9fe4018e046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Fri, 16 Oct 2020 23:03:19 +0200 Subject: Minor JS cleanup. --- static/lib.js | 14 ++++++++++++++ static/script.js | 38 ++++++++++++++------------------------ 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/static/lib.js b/static/lib.js index 3c11e23f..79e48f1e 100644 --- a/static/lib.js +++ b/static/lib.js @@ -131,4 +131,18 @@ function format_date(date, str) { Object.prototype.format = function () { return this; } /* any number of arguments */ Date.prototype.format = function (str) { return format_date (this, str); } +/* + * Finds the first element of the DOMTokenList whichs value matches + * the supplied regexp. Returns a pair of the index and the value. + */ +DOMTokenList.prototype.find = function (regexp) { + let entries = this.entries(); + let entry; + while (! (entry = entries.next()).done) { + if (entry.value[1].match(regexp)) { + return entry.value; + } + } +} + const xcal = "urn:ietf:params:xml:ns:icalendar-2.0"; diff --git a/static/script.js b/static/script.js index 636bf086..d7e60488 100644 --- a/static/script.js +++ b/static/script.js @@ -10,6 +10,9 @@ let parser = new DOMParser(); let start_time = new Date(); let end_time = new Date(); +/* + Given the navbar of a popup, make it dragable. + */ function bind_popup_control (nav) { nav.onmousedown = function (e) { /* Ignore mousedown on children */ @@ -36,20 +39,6 @@ function bind_popup_control (nav) { }); } -/* - * Finds the first element of the DOMTokenList whichs value matches - * the supplied regexp. Returns a pair of the index and the value. - */ -DOMTokenList.prototype.find = function (regexp) { - let entries = this.entries(); - let entry; - while (! (entry = entries.next()).done) { - if (entry.value[1].match(regexp)) { - return entry.value; - } - } -} - class EventCreator { /* dynamicly created event when dragging */ @@ -61,8 +50,10 @@ class EventCreator { } create_empty_event () { - let event = document.getElementById("event-template").firstChild.cloneNode(true); - let popup = document.getElementById("popup-template").firstChild.cloneNode(true); + let event = document.getElementById("event-template") + .firstChild.cloneNode(true); + let popup = document.getElementById("popup-template") + .firstChild.cloneNode(true); popup.getElementsByClassName("edit-form")[0].onsubmit = function () { create_event(event); @@ -294,12 +285,6 @@ function update_current_time_bar () { = (new Date).format("~Y-~m-~d") + ".html"; } -function close_all_popups () { - for (let popup of document.querySelectorAll(".popup-container.visible")) { - close_popup(popup); - } -} - async function create_event (event) { let xml = event.getElementsByTagName("icalendar")[0].outerHTML @@ -325,7 +310,7 @@ async function create_event (event) { let body = await response.text(); - /* servere is assumed to return an XML document on the form + /* server is assumed to return an XML document on the form **xcal property** ... @@ -398,7 +383,6 @@ window.onload = function () { end_time.setTime(document.querySelector("meta[name='end-time']").content * 1000) update_current_time_bar() - // once a minute for now, could probably be slowed to every 10 minutes window.setInterval(update_current_time_bar, 1000 * 60) /* Is event creation active? */ @@ -668,6 +652,12 @@ function close_popup(popup) { popup.classList.remove("visible"); } +function close_all_popups () { + for (let popup of document.querySelectorAll(".popup-container.visible")) { + close_popup(popup); + } +} + function open_popup(popup) { popup.classList.add("visible"); let element = document.getElementById(popup.id.substr(5)) -- cgit v1.2.3 From 81740400f9bcf10a384f3f143a02b3bdeba0c2fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Fri, 16 Oct 2020 23:09:11 +0200 Subject: s/inline-edit/input-list/ --- module/calp/html/vcomponent.scm | 2 +- static/script.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/module/calp/html/vcomponent.scm b/module/calp/html/vcomponent.scm index 7414654b..cf8f3a9d 100644 --- a/module/calp/html/vcomponent.scm +++ b/module/calp/html/vcomponent.scm @@ -203,7 +203,7 @@ ,@(with-label "Kategorier" - `(div (@ (class "inline-edit")) + `(div (@ (class "input-list")) ,@(awhen (prop ev 'CATEGORIES) (map (lambda (c) `(input (@ (size 2) diff --git a/static/script.js b/static/script.js index d7e60488..34ccdb2a 100644 --- a/static/script.js +++ b/static/script.js @@ -491,7 +491,7 @@ window.onload = function () { */ - for (let el of document.querySelectorAll(".inline-edit input")) { + for (let el of document.querySelectorAll(".input-list input")) { el.oninput = update_inline_list; } @@ -969,7 +969,7 @@ function advance_final(li) { size: 2, oninput: li.oninput, }); - li.closest(".inline-edit").appendChild(new_li); + li.closest(".input-list").appendChild(new_li); } function update_inline_list () { -- cgit v1.2.3 From 28c560c4c11f51b2dfffc77a286ad03057e4a13b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Fri, 23 Oct 2020 00:14:17 +0200 Subject: Work on generalizing multi-input lists. --- module/calp/html/vcomponent.scm | 23 +++++++------- static/script.js | 68 +++++++++++++++++++++++++++++++---------- static/style.scss | 5 +++ 3 files changed, 69 insertions(+), 27 deletions(-) diff --git a/module/calp/html/vcomponent.scm b/module/calp/html/vcomponent.scm index cf8f3a9d..208b66f7 100644 --- a/module/calp/html/vcomponent.scm +++ b/module/calp/html/vcomponent.scm @@ -207,27 +207,28 @@ ,@(awhen (prop ev 'CATEGORIES) (map (lambda (c) `(input (@ (size 2) + (class "unit") (value ,c)))) it)) - (input (@ (class "final") + (input (@ (class "unit final") (size 2) (type "text") )))) - ;; TODO extra fields - (hr) - (div (@ (class "newfield")) - (input (@ (type "text") - (list "known-fields") - (placeholder "Nytt fält"))) - (select (@ (name "TYPE")) - (option (@ (value "TEXT")) "Text")) - (span + ;; For custom user fields + (div (@ (class "input-list")) + (div (@ (class "unit final newfield")) (input (@ (type "text") - (placeholder "Värde"))))) + (list "known-fields") + (placeholder "Nytt fält"))) + (select (@ (name "TYPE")) + (option (@ (value "TEXT")) "Text")) + (span + (input (@ (type "text") + (placeholder "Värde")))))) (hr) diff --git a/static/script.js b/static/script.js index 34ccdb2a..42f61da7 100644 --- a/static/script.js +++ b/static/script.js @@ -490,10 +490,19 @@ window.onload = function () { serializer.serializeToString(xml); */ + for (let lst of document.getElementsByClassName('input-list')) { + let unit = lst.querySelector('.final.unit').cloneNode(true); + lst.unit = unit; + for (let el of lst.getElementsByTagName('input')) { + el.addEventListener('input', update_inline_list); + } + } + /* for (let el of document.querySelectorAll(".input-list input")) { el.oninput = update_inline_list; } + */ for (let el of document.getElementsByClassName("newfield")) { @@ -619,7 +628,7 @@ window.onload = function () { } } - name.oninput = function () { + name.addEventListener('input', function () { let types = valid_input_types[this.value.toUpperCase()]; type_selector.disabled = false; if (types) { @@ -640,7 +649,7 @@ window.onload = function () { } update_value_field(el); - } + }); type_selector.onchange = function () { update_value_field(el); } @@ -783,9 +792,9 @@ function bind_properties (el, wide_event=false) { /* edit tab */ for (let e of popup.querySelectorAll(".edit-tab .bind")) { let p = get_property(el, e.dataset.property); - e.oninput = function () { + e.addEventListener('input', function () { el.properties[e.dataset.property] = this.value; - } + }); let f; switch (e.tagName) { case 'input': @@ -961,26 +970,53 @@ function bind_properties (el, wide_event=false) { } +/* + TODO document 'input-list'. -function advance_final(li) { - li.classList.remove("final"); - let new_li = makeElement ('input', { - className: 'final', - size: 2, - oninput: li.oninput, - }); - li.closest(".input-list").appendChild(new_li); + ∀ children('.input-list') => 'unit' ∈ classList(child) + +
+
+
+
+ +*/ + + +function advance_final(input_list) { + let new_unit = input_list.unit.cloneNode(true); + new_unit.classList.add('final'); + + for (let i of new_unit.getElementsByTagName('input')) { + /* TODO eventual other listeners? */ + /* TODO