aboutsummaryrefslogtreecommitdiff
path: root/static/script.js
diff options
context:
space:
mode:
Diffstat (limited to 'static/script.js')
-rw-r--r--static/script.js619
1 files changed, 403 insertions, 216 deletions
diff --git a/static/script.js b/static/script.js
index ce57e5e1..60011cd8 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,15 @@ 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);
+ return false; /* stop default */
+ }
let id = gensym ("__js_event");
@@ -156,7 +152,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);
/* ---------------------------------------- */
@@ -217,6 +213,8 @@ class EventCreator {
e.style.pointerEvents = "";
}
+ place_in_edit_mode(that.event);
+
let localevent = that.event;
that.event = null;
@@ -287,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
@@ -318,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
<properties>
**xcal property** ...
</properties>
@@ -345,142 +337,30 @@ async function create_event (event) {
toggle_popup("popup" + event.id);
}
+/* This incarnation of this function only adds the calendar switcher dropdown.
+ All events are already editable by switching to that tab.
+
+ TODO stop requiring a weird button press to change calendar.
+*/
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])
-
- description.replaceWith(textarea);
- }
-
- /* ---------------------------------------- */
-
- let evtext = popup.getElementsByClassName('eventtext')[0]
+ let container = popup.getElementsByClassName('dropdown-goes-here')[0]
let calendar_dropdown = document.getElementById('calendar-dropdown-template').firstChild.cloneNode(true);
let [_, calclass] = popup.classList.find(/^CAL_/);
- label: {
- for (let [i, option] of calendar_dropdown.childNodes.entries()) {
- if (option.value === calclass.substr(4)) {
- calendar_dropdown.selectedIndex = i;
- break label;
- }
- }
- /* no match, try find default calendar */
- let t;
- if ((t = calendar_dropdown.querySelector("[selected]"))) {
- event.properties.calendar = t.value;
- }
+ 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
@@ -489,32 +369,12 @@ 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);
+ let tab = popup.getElementsByClassName("tab")[1];
+ let radio = tab.getElementsByTagName("input")[0];
+ radio.click();
+ tab.querySelector("input[name='summary']").focus();
}
window.onload = function () {
@@ -522,7 +382,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? */
@@ -575,6 +434,13 @@ window.onload = function () {
*/
el.parentElement.removeAttribute("href");
+ /* TODO this doesn't yet apply to newly created events */
+ let popup = document.getElementById("popup" + el.id);
+ popup.getElementsByClassName("edit-form")[0].onsubmit = function () {
+ create_event(el);
+ return false; /* stop default */
+ }
+
/* Bind all vcomponent properties into javascript. */
if (el.closest(".longevents")) {
bind_properties(el, true);
@@ -623,15 +489,212 @@ window.onload = function () {
serializer.serializeToString(xml);
*/
+ /* needs to be called AFTER bind_properties, but BEFORE init_input_list
+ After bind_properties since that initializes categories to a possible field
+ Before init_input_list since we need this listener to be propagated to clones.
+ [CATEGORIES_BIND]
+ */
+ for (let lst of document.querySelectorAll(".input-list[data-property='categories']")) {
+ let f = function () {
+ console.log(lst, lst.closest('.popup-container'));
+ let event = event_from_popup(lst.closest('.popup-container'))
+ event.properties.categories = [...lst.querySelectorAll('input')]
+ .map(x => x.value)
+ .filter(x => x != '')
+ .join(',');
+
+ };
+
+ for (let inp of lst.querySelectorAll('input')) {
+ inp.addEventListener('input', f);
+ }
+ }
+
+
+ /* ---------------------------------------- */
+
+
+ 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 'boolean':
+ value.type = 'checkbox';
+ 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.addEventListener('input', function setOptionDropdown () {
+ let types = valid_input_types[this.value.toUpperCase()];
+ let el = this.parentElement;
+ let [_, type_selector, value_field] = el.children;
+
+ 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.addEventListener('change', function () {
+ update_value_field(this.parentElement);
+ });
+ }
+
+
+
+ init_input_list();
+
+}
+
+function event_from_popup(popup) {
+ return document.getElementById(popup.id.substr(5))
+}
+
+function popup_from_event(event) {
+ return document.getElementById("popup" + event.id);
}
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))
+ let element = event_from_popup(popup);
// let root = document.body;
let root;
switch (VIEW) {
@@ -674,7 +737,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 = {};
}
@@ -698,24 +761,29 @@ 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;}
+ return el.properties["_slot_" + field];
+}
+
- ical_properties.appendChild(element);
- el.properties["_slot_" + field].push(
- [text, (s, v) => s.innerHTML = v]);
+
+/*
+class display_tab {
+}
+
+class edit_tab {
+}
+
+class vcomponent {
+ set_value(field, value) {
+ if (value === '') {
+ remove_property(field);
}
}
-
- return el.properties["_slot_" + field];
}
+*/
+
/*
Properties are icalendar properties.
@@ -728,39 +796,155 @@ function get_property(el, field, default_value, bind_to_ical=true) {
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 popup = popup_from_event(el);
+ // let children = el.getElementsByTagName("properties")[0].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 */
+
+ 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]);
+ }
- /* 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]);
+ /* edit tab */
+ for (let e of popup.querySelectorAll(".edit-tab .bind")) {
+ let p = get_property(el, e.dataset.property);
+ e.addEventListener('input', function () {
+ el.properties[e.dataset.property] = this.value;
+ });
+ let f;
+ switch (e.tagName) {
+ case 'input':
+ switch (e.type) {
+ 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;
+ }
+ 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;
}
- for (let s of popup.getElementsByClassName(field)) {
- let f = ((s, v) => s.innerHTML = v.format(s.dataset && s.dataset.fmt));
- lst.push([s, f]);
+ }
+
+ /* 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']) {
+
+ 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.setMonth(Number(m) - 1);
+ date.setDate(d);
+ 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");
+ popup
+ .querySelector(`.edit-tab input[name='${field}-date']`)
+ .value = v.format("~Y-~m-~d");
+ }]);
+ }
+
+
+ /* icalendar properties */
+ for (let child of el.querySelector("vevent > properties").children) {
+ /* child ≡ <dtstart><date-time>...</date-time></dtstart> */
+
+ 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:~S~Z")]); break;
- default:
- lst.push([s, (s, v) => s.innerHTML = v]);
- }
+ for (let s of el.querySelectorAll(`${field} > :not(parameters)`)) {
+ lst.push([s, (s, v) => {
+ if (v instanceof Date) {
+ if (v.isWholeDay) {
+ let str = v.format('~Y-~m-~d');
+ child.innerHTML = `<date>${str}</date>`;
+ } else {
+ let str = v.format('~Y-~m-~dT~H:~M:00~Z');
+ child.innerHTML = `<date-time>${str}</date-time>`;
+ }
+ } else {
+ /* assume that type already is correct */
+ s.innerHTML = v;
+ }
+ }]);
el.properties["_value_" + field] = s.innerHTML;
}
}
+ /* Dynamicly add or remove the <location/> and <description/> elements
+ from the <vevent><properties/> list.
+
+ TODO generalize this to all fields, /especially/ those which are
+ dynamicly added.
+ */
+ for (let field of ['location', 'description', 'categories']) {
+ 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}><text/></${field}>`;
+ }
+ s.querySelector(`${field} > text`).innerHTML = v;
+ }
+ }]);
+ }
+
+ /* set up graphical display changes */
let container = el.closest(".event-container");
if (container === null) {
console.log("No enclosing event container for", el);
@@ -793,7 +977,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_/);
@@ -804,4 +988,7 @@ function bind_properties (el, wide_event=false) {
calprop.push([el, rplcs]);
calprop.push([el, (s, v) => s.dataset.calendar = v]);
+
+
+ /* ---------- Calendar ------------------------------ */
}