From 1b5ff103f589140473068fd83340b0cc443fb420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Fri, 20 Nov 2020 23:01:32 +0100 Subject: Work on templetazing js. --- static/arbitary_kv.js | 160 ++++++++++++++++++ static/clock.js | 70 ++++++++ static/dragable.js | 41 +++++ static/input_list.js | 5 +- static/popup.js | 61 +++++++ static/script.js | 418 +++-------------------------------------------- static/server_connect.js | 75 +++++++++ 7 files changed, 429 insertions(+), 401 deletions(-) create mode 100644 static/arbitary_kv.js create mode 100644 static/clock.js create mode 100644 static/dragable.js create mode 100644 static/popup.js create mode 100644 static/server_connect.js (limited to 'static') diff --git a/static/arbitary_kv.js b/static/arbitary_kv.js new file mode 100644 index 00000000..baf387eb --- /dev/null +++ b/static/arbitary_kv.js @@ -0,0 +1,160 @@ +/* + The freeform key-value fields at the bottom of any popup. + */ + +function init_arbitary_kv() { + 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); + }); + } + +} diff --git a/static/clock.js b/static/clock.js new file mode 100644 index 00000000..badfd1db --- /dev/null +++ b/static/clock.js @@ -0,0 +1,70 @@ + +class Clock { + update(now) { + } +} + + +class Timebar extends Clock { + + constructor(start_time, end_time) { + super(); + this.start_time = start_time; + this.end_time = end_time; + this.bar_object = false + } + + + update(now) { + if (! (this.start_time <= now.getTime() && now.getTime() < this.end_time)) + return; + + var event_area = document.getElementById(now.format("~Y-~m-~d")) + + if (event_area) { + if (this.bar_object) { + this.bar_object.parentNode.removeChild(bar_object) + } else { + this.bar_object = makeElement ('div', { + id: 'bar', + className: 'eventlike current-time', + }); + } + + this.bar_object.style.top = date_to_percent(now) + "%"; + event_area.append(this.bar_object) + } + } +} + +class SmallcalCellHighlight extends Clock { + constructor(small_cal) { + super(); + this.small_cal = small_cal; + this.current_cell = false + } + + update(now) { + if (current_cell) { + current_cell.style.border = ""; + } + + current_cell = this.small_cal.querySelector( + "time[datetime='" + now.format("~Y-~m-~d") + "']"); + + current_cell.style.border = "1px solid black"; + } +} + +/* Update [today] button */ +class ButtonUpdater extends Clock { + constructor(el, proc) { + super(); + this.el = el; + this.proc = proc; + } + + update(now) { + this.proc(e, now); + } +} diff --git a/static/dragable.js b/static/dragable.js new file mode 100644 index 00000000..41895760 --- /dev/null +++ b/static/dragable.js @@ -0,0 +1,41 @@ +/* + Apply to a given component to make it draggable. + Drag area (usually a title bar) should be be the only argument. + It is REQUIRED that the object which should be moved have the class + 'popup-container'; +*/ + + +/* + Given the navbar of a popup, make it dragable. + */ +function bind_popup_control (nav) { + + if (! nav.closest('.popup-container')) { + throw TypeError('not a popup container'); + } + + nav.onmousedown = function (e) { + /* Ignore mousedown on children */ + if (e.target != nav) return; + nav.style.cursor = "grabbing"; + nav.dataset.grabbed = "true"; + nav.dataset.grabPoint = e.clientX + ";" + e.clientY; + let popup = nav.closest(".popup-container"); + nav.dataset.startPoint = popup.offsetLeft + ";" + popup.offsetTop; + } + window.addEventListener('mousemove', function (e) { + if (nav.dataset.grabbed) { + let [x, y] = nav.dataset.grabPoint.split(";").map(Number); + let [startX, startY] = nav.dataset.startPoint.split(";").map(Number); + let popup = nav.closest(".popup-container"); + + popup.style.left = startX + (e.clientX - x) + "px"; + popup.style.top = startY + (e.clientY - y) + "px"; + } + }); + window.addEventListener('mouseup', function () { + nav.dataset.grabbed = ""; + nav.style.cursor = ""; + }); +} diff --git a/static/input_list.js b/static/input_list.js index 79e223c3..3b24b719 100644 --- a/static/input_list.js +++ b/static/input_list.js @@ -1,6 +1,4 @@ /* - TODO document 'input-list'. - ∀ children('.input-list') => 'unit' ∈ classList(child)
@@ -78,7 +76,6 @@ function init_input_list() { lst.get_value = lst.dataset.bindby; } else if (lst.dataset.joinby) { lst.get_value = get_value(lst.dataset.joinby); - } } else { lst.get_value = get_get_value(); } @@ -88,7 +85,7 @@ function init_input_list() { /* -------------------------------------------------- */ /* different function forms since we want to capture one self */ -const get_get_value(join=',') => function () { +const get_get_value = (join=',') => function () { return [...self.querySelectorAll('input')] .map(x => x.value) .filter(x => x != '') diff --git a/static/popup.js b/static/popup.js new file mode 100644 index 00000000..bc4e766d --- /dev/null +++ b/static/popup.js @@ -0,0 +1,61 @@ + + +/* event component => coresponding popup component */ +function event_from_popup(popup) { + return document.getElementById(popup.id.substr(5)) +} + +/* popup component => coresponding event component */ +function popup_from_event(event) { + return document.getElementById("popup" + event.id); +} + +/* hides given popup */ +function close_popup(popup) { + popup.classList.remove("visible"); +} + +/* hides all popups */ +function close_all_popups () { + for (let popup of document.querySelectorAll(".popup-container.visible")) { + close_popup(popup); + } +} + +/* open given popup */ +function open_popup(popup) { + popup.classList.add("visible"); + let element = event_from_popup(popup); + // let root = document.body; + let root; + switch (VIEW) { + case 'week': + root = document.getElementsByClassName("days")[0]; + break; + case 'month': + default: + root = document.body; + break; + } + /* start sets offset between top left corner + of event in calendar and popup. 10, 10 soo old + event is still visible */ + let offsetX = 10, offsetY = 10; + while (element !== root) { + offsetX += element.offsetLeft; + offsetY += element.offsetTop; + element = element.offsetParent; + } + popup.style.left = offsetX + "px"; + popup.style.top = offsetY + "px"; +} + +/* toggles open/closed status of popup given by id */ +function toggle_popup(popup_id) { + let popup = document.getElementById(popup_id); + if (popup.classList.contains("visible")) { + close_popup(popup); + } else { + open_popup(popup); + } +} diff --git a/static/script.js b/static/script.js index 60011cd8..74c90e27 100644 --- a/static/script.js +++ b/static/script.js @@ -4,41 +4,6 @@ calp specific stuff */ -let parser = new DOMParser(); - -/* start and end time for calendar page */ -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 */ - if (e.target != nav) return; - nav.style.cursor = "grabbing"; - nav.dataset.grabbed = "true"; - nav.dataset.grabPoint = e.clientX + ";" + e.clientY; - let popup = nav.closest(".popup-container"); - nav.dataset.startPoint = popup.offsetLeft + ";" + popup.offsetTop; - } - window.addEventListener('mousemove', function (e) { - if (nav.dataset.grabbed) { - let [x, y] = nav.dataset.grabPoint.split(";").map(Number); - let [startX, startY] = nav.dataset.startPoint.split(";").map(Number); - let popup = nav.closest(".popup-container"); - - popup.style.left = startX + (e.clientX - x) + "px"; - popup.style.top = startY + (e.clientY - y) + "px"; - } - }); - window.addEventListener('mouseup', function () { - nav.dataset.grabbed = ""; - nav.style.cursor = ""; - }); -} - class EventCreator { /* dynamicly created event when dragging */ @@ -46,7 +11,6 @@ class EventCreator { this.event = false; this.event_start = { x: NaN, y: NaN }; this.down_on_event = false; - } create_empty_event () { @@ -152,7 +116,6 @@ class EventCreator { bind_properties(event, wide_element); /* requires that dtstart and dtend properties are initialized */ - // place_in_edit_mode(event); /* ---------------------------------------- */ @@ -224,119 +187,6 @@ class EventCreator { } } -async function remove_event (element) { - let uid = element.querySelector("icalendar uid text").innerHTML; - - let data = new URLSearchParams(); - data.append('uid', uid); - - let response = await fetch ( '/remove', { - method: 'POST', - body: data - }); - - console.log(response); - toggle_popup("popup" + element.id); - - if (response.status < 200 || response.status >= 300) { - let body = await response.text(); - alert(`HTTP error ${response.status}\n${body}`) - } else { - element.remove(); - } -} - -var bar_object = false -var current_cell = false - -function update_current_time_bar () { - var now = new Date() - if (! (start_time <= now.getTime() && now.getTime() < end_time)) - return; - - var event_area = document.getElementById(now.format("~Y-~m-~d")) - - if (event_area) { - if (bar_object) { - bar_object.parentNode.removeChild(bar_object) - } else { - bar_object = makeElement ('div', { - id: 'bar', - className: 'eventlike current-time', - }); - } - - bar_object.style.top = date_to_percent(now) + "%"; - event_area.append(bar_object) - } - - /* */ - - if (current_cell) { - current_cell.style.border = ""; - } - current_cell = document.querySelector( - ".small-calendar time[datetime='" + now.format("~Y-~m-~d") + "']"); - current_cell.style.border = "1px solid black"; - - /* Update [today] button */ - - document.getElementById("today-button").href - = (new Date).format("~Y-~m-~d") + ".html"; -} - -async function create_event (event) { - - let xml = event.getElementsByTagName("icalendar")[0].outerHTML - let calendar = event.properties.calendar; - - console.log(calendar, xml); - - let data = new URLSearchParams(); - data.append("cal", calendar); - data.append("data", xml); - - let response = await fetch ( '/insert', { - method: 'POST', - body: data - }); - - console.log(response); - if (response.status < 200 || response.status >= 300) { - let body = await response.text(); - alert(`HTTP error ${response.status}\n${body}`) - return; - } - - let body = await response.text(); - - /* server is assumed to return an XML document on the form - - **xcal property** ... - - parse that, and update our own vevent with the data. - */ - - let properties = parser - .parseFromString(body, 'text/xml') - .children[0]; - - let child; - while ((child = properties.firstChild)) { - let target = event.querySelector( - "vevent properties " + child.tagName); - if (target) { - target.replaceWith(child); - } else { - event.querySelector("vevent properties") - .appendChild(child); - } - } - - event.classList.remove("generated"); - 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. @@ -378,11 +228,26 @@ function place_in_edit_mode (event) { } window.onload = function () { - start_time.setTime(document.querySelector("meta[name='start-time']").content * 1000) - end_time.setTime(document.querySelector("meta[name='end-time']").content * 1000) + let start_time = document.querySelector("meta[name='start-time']").content; + let end_time = document.querySelector("meta[name='end-time']").content; + + const button_updater = new ButtonUpdater( + document.getElementById("today-button"), + (e, d) => e.href = d.format('~Y-~m-~d') + ".html" + ); + + const sch = new SmallcalCellHighlight( + document.querySelector('.small-calendar')) - update_current_time_bar() - window.setInterval(update_current_time_bar, 1000 * 60) + const timebar = new Timebar(start_time, end_time); + + timebar.update(new Date); + window.setInterval(() => { + let d = new Date; + timebar.update(d); + button_updater.update(d); + sch.update(d); + }, 1000 * 60); /* Is event creation active? */ if (EDIT_MODE) { @@ -483,12 +348,6 @@ window.onload = function () { /* ---------------------------------------- */ - /* - xml.querySelector("summary text").innerHTML = "Pastahack"; - let serializer = new XMLSerializer(); - 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. @@ -498,11 +357,7 @@ window.onload = function () { 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(','); - + event.properties.categories = lst.get_value(); }; for (let inp of lst.querySelectorAll('input')) { @@ -510,225 +365,12 @@ window.onload = function () { } } - - /* ---------------------------------------- */ - - - 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_arbitary_kv(); 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 = event_from_popup(popup); - // let root = document.body; - let root; - switch (VIEW) { - case 'week': - root = document.getElementsByClassName("days")[0]; - break; - case 'month': - default: - root = document.body; - break; - } - /* start sets offset between top left corner - of event in calendar and popup. 10, 10 soo old - event is still visible */ - let offsetX = 10, offsetY = 10; - while (element !== root) { - offsetX += element.offsetLeft; - offsetY += element.offsetTop; - element = element.offsetParent; - } - popup.style.left = offsetX + "px"; - popup.style.top = offsetY + "px"; -} - -function toggle_popup(popup_id) { - let popup = document.getElementById(popup_id); - if (popup.classList.contains("visible")) { - close_popup(popup); - } else { - open_popup(popup); - } -} - - /* Returns the _value_ slot of given field in event, creating it if needed . @@ -766,24 +408,6 @@ function get_property(el, field, default_value) { -/* -class display_tab { -} - -class edit_tab { -} - -class vcomponent { - set_value(field, value) { - if (value === '') { - remove_property(field); - } - } -} - - -*/ - /* Properties are icalendar properties. diff --git a/static/server_connect.js b/static/server_connect.js new file mode 100644 index 00000000..e789d72c --- /dev/null +++ b/static/server_connect.js @@ -0,0 +1,75 @@ + +async function remove_event (element) { + let uid = element.querySelector("icalendar uid text").innerHTML; + + let data = new URLSearchParams(); + data.append('uid', uid); + + let response = await fetch ( '/remove', { + method: 'POST', + body: data + }); + + console.log(response); + toggle_popup("popup" + element.id); + + if (response.status < 200 || response.status >= 300) { + let body = await response.text(); + alert(`HTTP error ${response.status}\n${body}`) + } else { + element.remove(); + } +} + +async function create_event (event) { + + let xml = event.getElementsByTagName("icalendar")[0].outerHTML + let calendar = event.properties.calendar; + + console.log(calendar, xml); + + let data = new URLSearchParams(); + data.append("cal", calendar); + data.append("data", xml); + + let response = await fetch ( '/insert', { + method: 'POST', + body: data + }); + + console.log(response); + if (response.status < 200 || response.status >= 300) { + let body = await response.text(); + alert(`HTTP error ${response.status}\n${body}`) + return; + } + + let body = await response.text(); + + /* server is assumed to return an XML document on the form + + **xcal property** ... + + parse that, and update our own vevent with the data. + */ + + let parser = new DOMParser(); + let properties = parser + .parseFromString(body, 'text/xml') + .children[0]; + + let child; + while ((child = properties.firstChild)) { + let target = event.querySelector( + "vevent properties " + child.tagName); + if (target) { + target.replaceWith(child); + } else { + event.querySelector("vevent properties") + .appendChild(child); + } + } + + event.classList.remove("generated"); + toggle_popup("popup" + event.id); +} -- cgit v1.2.3