aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--module/entry-points/server.scm3
-rw-r--r--module/html/vcomponent.scm88
-rw-r--r--module/html/view/calendar.scm18
-rw-r--r--module/vcomponent/parse/xcal.scm31
-rw-r--r--static/script.js86
-rw-r--r--static/style.css26
6 files changed, 191 insertions, 61 deletions
diff --git a/module/entry-points/server.scm b/module/entry-points/server.scm
index 466860cd..dc675813 100644
--- a/module/entry-points/server.scm
+++ b/module/entry-points/server.scm
@@ -163,6 +163,9 @@
(format #f "No event with UID '~a'" uid))))
;; TODO this fails when dtstart is <date>.
+ ;; TODO If data has an explicit UID and that UID already exists we
+ ;; overwrite it in the database. We however don't remove the old
+ ;; event from the in-memory set, but rather just adds the new.
(POST "/insert" (cal data)
(unless (and cal data)
diff --git a/module/html/vcomponent.scm b/module/html/vcomponent.scm
index 9189b59e..fdaea217 100644
--- a/module/html/vcomponent.scm
+++ b/module/html/vcomponent.scm
@@ -44,18 +44,21 @@
;; TODO better format, add show in calendar button
,(fmt-single-event event)))))
-;; For sidebar, just text
+;; Format event as text.
+;; Used in
+;; - sidebar
+;; - popup overwiew tab
+;; - search result (event details)
(define*-public (fmt-single-event ev
optional: (attributes '())
key: (fmt-header list))
;; (format (current-error-port) "fmt-single-event: ~a~%" (prop ev 'X-HNH-FILENAME))
`(article (@ ,@(assq-merge
attributes
- `((class "eventtext CAL_bg_"
- ,(html-attr (or (prop (parent ev) 'NAME) "unknown"))
+ `((class " eventtext "
,(when (and (prop ev 'PARTSTAT)
(eq? 'TENTATIVE (prop ev 'PARTSTAT)))
- " tentative")))))
+ " tentative ")))))
(h3 ,(fmt-header
(when (prop ev 'RRULE)
`(span (@ (class "repeating")) "↺"))
@@ -79,8 +82,9 @@
(div (@ (class "location"))
,(string-map (lambda (c) (if (char=? c #\,) #\newline c))
(prop ev 'LOCATION)))))
- ,(and=> (prop ev 'DESCRIPTION)
- (lambda (str) (format-description ev str)))
+ ,(awhen (prop ev 'DESCRIPTION)
+ `(span (@ (class "description"))
+ ,(format-description ev it)))
,(awhen (prop ev 'RRULE)
`(span (@ (class "rrule"))
,@(format-recurrence-rule ev)))
@@ -100,13 +104,15 @@
(class "hidelink")) ,s))))
,@(stream->list
(stream-map
- (lambda (ev) (fmt-single-event
- ev `((id ,(html-id ev)))
- fmt-header:
- (lambda body
- `(a (@ (href "#" ,(date-link (as-date (prop ev 'DTSTART))))
- (class "hidelink"))
- ,@body))))
+ (lambda (ev)
+ (fmt-single-event
+ ev `((id ,(html-id ev))
+ (class "CAL_" ,(html-attr (or (prop (parent ev) 'NAME) "unknown"))))
+ fmt-header:
+ (lambda body
+ `(a (@ (href "#" ,(date-link (as-date (prop ev 'DTSTART))))
+ (class "hidelink"))
+ ,@body))))
(stream-filter
(lambda (ev)
;; If start was an earlier day
@@ -119,16 +125,14 @@
(define-public (calendar-styles calendars)
`(style
- ,(format
- #f "~:{.CAL_~a { background-color: ~a; color: ~a }~%.CAL_bg_~a { border-color: ~a }~%~}"
- (map (lambda (c)
- (let* ((name (html-attr (prop c 'NAME)))
- (bg-color (prop c 'COLOR))
- (fg-color (and=> (prop c 'COLOR)
- calculate-fg-color)))
- (list name (or bg-color 'white) (or fg-color 'black)
- name (or bg-color 'black))))
- calendars))))
+ ,(format #f "~:{.CAL_~a { --color: ~a; --complement: ~a }~%~}"
+ (map (lambda (c)
+ (let* ((name (html-attr (prop c 'NAME)))
+ (bg-color (prop c 'COLOR))
+ (fg-color (and=> (prop c 'COLOR)
+ calculate-fg-color)))
+ (list name (or bg-color 'white) (or fg-color 'black))))
+ calendars))))
;; "Physical" block in calendar view
(define*-public (make-block ev optional: (extra-attributes '()))
@@ -165,28 +169,34 @@
(define-public (popup ev id)
- `(div (@ (class "popup-container") (id ,id)
+ `(div (@ (id ,id) (class "popup-container CAL_"
+ ,(html-attr (or (prop (parent ev) 'NAME)
+ "unknown")))
(onclick "event.stopPropagation()"))
+ ;; TODO all (?) code uses .popup-container as the popup, while .popup sits and does nothing.
+ ;; Do something about this?
(div (@ (class "popup"))
- (nav (@ (class "popup-control CAL_"
- ,(html-attr (or (prop (parent ev) 'NAME)
- "unknown"))))
+ (nav (@ (class "popup-control"))
,(btn "×"
title: "Stäng"
onclick: "close_popup(document.getElementById(this.closest('.popup-container').id))"
class: '("close-tooltip"))
,(when (edit-mode)
- (btn "🗑"
- title: "Ta bort"
- onclick: "remove_event(document.getElementById(this.closest('.popup-container').id.substr(5)))")))
+ (list
+ (btn "🖊️"
+ title: "Redigera"
+ onclick: "place_in_edit_mode(document.getElementById(this.closest('.popup-container').id.substr(5)))")
+ (btn "🗑"
+ title: "Ta bort"
+ onclick: "remove_event(document.getElementById(this.closest('.popup-container').id.substr(5)))"))))
,(tabset
- `(("📅" title: "Översikt"
- ,(fmt-single-event ev))
- ("⤓" title: "Nedladdning"
- (div (@ (style "font-family:sans"))
- (p "Ladda ner")
- (ul (li (a (@ (href "/calendar/" ,(prop ev 'UID) ".ics"))
- "som iCal"))
- (li (a (@ (href "/calendar/" ,(prop ev 'UID) ".xcs"))
- "som xCal"))))))))))
+ `(("📅" title: "Översikt"
+ ,(fmt-single-event ev))
+ ("⤓" title: "Nedladdning"
+ (div (@ (style "font-family:sans"))
+ (p "Ladda ner")
+ (ul (li (a (@ (href "/calendar/" ,(prop ev 'UID) ".ics"))
+ "som iCal"))
+ (li (a (@ (href "/calendar/" ,(prop ev 'UID) ".xcs"))
+ "som xCal"))))))))))
diff --git a/module/html/view/calendar.scm b/module/html/view/calendar.scm
index 2371cfe0..72fcccbd 100644
--- a/module/html/view/calendar.scm
+++ b/module/html/view/calendar.scm
@@ -284,10 +284,18 @@
(summary "Calendar list")
(ul ,@(map
(lambda (calendar)
- `(li (@ (class "CAL_bg_"
+ `(li (@ (class "CAL_"
,(html-attr (prop calendar 'NAME))))
,(prop calendar 'NAME)))
- calendars))))
+ calendars))
+ (div (@ (id "calendar-dropdown-template") (class "template"))
+ (select
+ (option "- Choose a Calendar -")
+ ,@(map (lambda (calendar)
+ `(option (@ (value ,(html-attr (prop calendar 'NAME))))
+ ,(prop calendar 'NAME)))
+ calendars))
+ )))
;; List of events
(div (@ (class "eventlist")
@@ -317,7 +325,11 @@
;; cloned mulitple times.
dtstart: (datetime)
dtend: (datetime)
- summary: "New Event"))))
+ summary: ""
+ ;; force a description field,
+ ;; but don't put anything in
+ ;; it.
+ description: ""))))
(event (car (children cal))))
`((div (@ (class "template event-container") (id "event-template")
;; Only needed to create a duration. So actual dates
diff --git a/module/vcomponent/parse/xcal.scm b/module/vcomponent/parse/xcal.scm
index 76bdb251..2c8b7fe8 100644
--- a/module/vcomponent/parse/xcal.scm
+++ b/module/vcomponent/parse/xcal.scm
@@ -22,6 +22,7 @@
[(boolean) (string=? "true" (car value))]
+ ;; TODO possibly trim whitespace on text fields
[(cal-address uri text unknown) (car value)]
[(date) (parse-iso-date (car value))]
@@ -126,21 +127,27 @@
(let ((params (handle-parameters parameters))
(tag* (symbol-upcase tag)))
(for (type value) in (zip type value)
- (set! (prop* component tag*)
- (make-vline tag*
- (handle-tag
- tag (handle-value type params value))
- params))))]
+ ;; ignore empty fields
+ ;; mostly for <text/>
+ (unless (null? value)
+ (set! (prop* component tag*)
+ (make-vline tag*
+ (handle-tag
+ tag (handle-value type params value))
+ params)))))]
[(tag (type value ...) ...)
(for (type value) in (zip type value)
- (let ((params (make-hash-table))
- (tag* (symbol-upcase tag)))
- (set! (prop* component tag*)
- (make-vline tag*
- (handle-tag
- tag (handle-value type params value))
- params))))])))
+ ;; ignore empty fields
+ ;; mostly for <text/>
+ (unless (null? value)
+ (let ((params (make-hash-table))
+ (tag* (symbol-upcase tag)))
+ (set! (prop* component tag*)
+ (make-vline tag*
+ (handle-tag
+ tag (handle-value type params value))
+ params)))))])))
;; children
(awhen (assoc-ref sxcal 'components)
diff --git a/static/script.js b/static/script.js
index 32bc3b4b..2c22f742 100644
--- a/static/script.js
+++ b/static/script.js
@@ -122,6 +122,20 @@ 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 */
@@ -428,6 +442,7 @@ function place_in_edit_mode (event) {
let input = makeElement ('input', {
type: "time",
required: true,
+ value: field.innerText,
onchange: function (e) {
/* Only update datetime when the input is filled out */
@@ -456,8 +471,9 @@ function place_in_edit_mode (event) {
let summary = popup.getElementsByClassName("summary")[0];
let input = makeElement('input', {
- name: "dtstart",
- placeholder: summary.innerText,
+ name: "summary",
+ value: summary.innerText,
+ placeholder: "Sammanfattning",
required: true,
});
@@ -473,6 +489,63 @@ function place_in_edit_mode (event) {
/* ---------------------------------------- */
+ /* 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");
+ if (descs.length === 1) {
+ let description = descs[0];
+ let textarea = makeElement('textarea', {
+ name: "description",
+ placeholder: "Description (optional)",
+ innerHTML: description.innerText,
+ required: false,
+ });
+
+ textarea.oninput = function () {
+ event.properties["description"] = this.value;
+ }
+
+ let slot = event.properties["_slot_description"]
+ let idx = slot.findIndex(e => e[0] === description);
+ slot.splice(idx, 1, [input, (s, v) => s.innerHTML = v])
+
+ description.replaceWith(textarea);
+ }
+
+ /* ---------------------------------------- */
+
+ let evtext = popup.getElementsByClassName('eventtext')[0]
+ let calendar_dropdown = document.getElementById('calendar-dropdown-template').firstChild.cloneNode(true);
+
+ let [_, calclass] = popup.classList.find(/^CAL_/);
+ for (let [i, option] of calendar_dropdown.childNodes.entries()) {
+ if (option.value === calclass.substr(4)) {
+ calendar_dropdown.selectedIndex = i;
+ break;
+ }
+ }
+
+ /* 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 () {
+ let popup = this.closest('.popup-container')
+ let event = document.getElementById(popup.id.substr(5))
+
+ let [_, calclass] = popup.classList.find(/^CAL_/);
+
+ popup.classList.replace(calclass, "CAL_" + this.value)
+ event.classList.replace(calclass, "CAL_" + this.value)
+
+
+ }
+ evtext.prepend(calendar_dropdown);
+
+ /* ---------------------------------------- */
+
let submit = makeElement( 'input', {
type: 'submit',
value: 'Skapa event',
@@ -490,6 +563,11 @@ function place_in_edit_mode (event) {
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();
+
}
window.onload = function () {
@@ -515,7 +593,7 @@ window.onload = function () {
let popupElement = document.getElementById("popup" + event.id);
open_popup(popupElement);
- popupElement.querySelector("input[name='dtstart']").focus();
+ popupElement.querySelector("input[name='summary']").focus();
});
}
@@ -533,7 +611,7 @@ window.onload = function () {
let popupElement = document.getElementById("popup" + event.id);
open_popup(popupElement);
- popupElement.querySelector("input[name='dtstart']").focus();
+ popupElement.querySelector("input[name='summary']").focus();
});
}
diff --git a/static/style.css b/static/style.css
index 193cc1fb..845443c2 100644
--- a/static/style.css
+++ b/static/style.css
@@ -300,6 +300,7 @@ along with their colors.
list-style-type: none;
border-left-width: 1em;
border-left-style: solid;
+ border-color: var(--color);
padding-left: 1ex;
/* force to single line */
@@ -507,9 +508,11 @@ along with their colors.
transition: 0.3s;
font-size: var(--event-font-size);
overflow: visible;
+ background-color: var(--color);
+ color: var(--complement);
}
-.event input {
+.popup input {
white-space: initial;
border: 1px solid gray;
max-width: 100%;
@@ -607,6 +610,10 @@ along with their colors.
padding-right: 1em;
}
+.eventlist .eventtext {
+ border-color: var(--color);
+}
+
.eventlist .eventtext.tentative {
border-left-style: dashed;
}
@@ -634,8 +641,16 @@ along with their colors.
}
+/*
+ * All other CAL_ classes are generated by the backend.
+ * NOTE Possibly move this there.
+ */
+.CAL_Generated {
+ background-color: #55FF55;
+}
+
.event.generated {
- background-color: #55FF5550;
+ opacity: 40%;
transition: none;
}
@@ -734,6 +749,11 @@ along with their colors.
user-select: none;
cursor: grab;
+ background-color: var(--color);
+ /* Transition for background color
+ * Matches that of '.event'.
+ * TODO break out to common place */
+ transition: 0.3s;
}
.popup-control .btn {
@@ -810,7 +830,7 @@ along with their colors.
}
.tab [type=radio]:checked ~ label ~ .content {
- z-index: 1;
+ z-index: 1;
}
/* Other