'use strict';
/*
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 */
constructor() {
this.event = false;
this.event_start = { x: NaN, y: NaN };
this.down_on_event = false;
}
create_empty_event () {
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?
// $("button 2??").onclick = `remove_event(document.getElementById('${id}'))`
let tabgroup_id = gensym();
for (let tab of popup.querySelectorAll(".tabgroup .tab")) {
let new_id = gensym();
let input = tab.querySelector("input");
input.id = new_id;
input.name = tabgroup_id;
tab.querySelector("label").setAttribute('for', new_id);
}
let nav = popup.getElementsByClassName("popup-control")[0];
bind_popup_control(nav);
// TODO download links
event.id = id;
popup.id = "popup" + id;
return [popup, event];
}
create_event_down (intended_target) {
let that = this;
return function (e) {
/* Only trigger event creation stuff on actuall events background,
NOT on its children */
that.down_on_event = false;
if (e.target != intended_target) return;
that.down_on_event = true;
that.event_start.x = e.clientX;
that.event_start.y = e.clientY;
}
}
/*
round_to: what start and end times should round to when dragging, in fractions
of the width of the containing container.
TODO limit this to only continue when on the intended event_container.
(event → [0, 1)), 𝐑, bool → event → ()
*/
create_event_move(pos_in, round_to=1, wide_element=false) {
let that = this;
return function (e) {
if (e.buttons != 1 || ! that.down_on_event) return;
/* Create event when we start moving the mouse. */
if (! that.event) {
/* Small deadzone so tiny click and drags aren't registered */
if (Math.abs(that.event_start.x - e.clientX) < 10
&& Math.abs(that.event_start.y - e.clientY) < 5)
{ return; }
/* only allow start of dragging on background */
if (e.target != this) return;
/* only on left click */
if (e.buttons != 1) return;
let [popup, event] = that.create_empty_event();
that.event = event;
/* TODO better solution to add popup to DOM */
document.getElementsByTagName("main")[0].append(popup);
/* [0, 1) -- where are we in the container */
/* Ronud to force steps of quarters */
/* NOTE for in-day events a floor here work better, while for
all day events I want a round, but which has the tip over point
around 0.7 instead of 0.5.
It might also be an idea to subtract a tiny bit from the short events
mouse position, since I feel I always get to late starts.
*/
let time = round_time(pos_in(this, e), round_to);
event.dataset.time1 = time;
event.dataset.time2 = time;
/* ---------------------------------------- */
this.appendChild(event);
/* requires that event is child of an '.event-container'. */
bind_properties(event, wide_element);
/* requires that dtstart and dtend properties are initialized */
// place_in_edit_mode(event);
/* ---------------------------------------- */
/* Makes all current events transparent when dragging over them.
Without this weird stuff happens when moving over them
This includes ourselves.
*/
for (let e of this.children) {
e.style.pointerEvents = "none";
}
}
let time1 = Number(that.event.dataset.time1);
let time2 = that.event.dataset.time2 =
round_time(pos_in(that.event.parentElement, e),
round_to);
/* ---------------------------------------- */
let event_container = that.event.closest(".event-container");
/* These two are in UTC */
let container_start = parseDate(event_container.dataset.start);
let container_end = parseDate(event_container.dataset.end);
/* ---------------------------------------- */
/* ms */
let duration = container_end - container_start;
let start_in_duration = duration * Math.min(time1,time2);
let end_in_duration = duration * Math.max(time1,time2);
/* Notice that these are converted to UTC, since the intervals are given
in utc, and I only really care about local time (which a specific local
timezone doesn't give me)
*/
/* TODO Should these inherit UTC from container_*? */
let d1 = new Date(container_start.getTime() + start_in_duration)
let d2 = new Date(container_start.getTime() + end_in_duration)
that.event.properties.dtstart = d1;
that.event.properties.dtend = d2;
}
}
create_event_finisher (callback) {
let that = this;
return function create_event_up (e) {
if (! that.event) return;
/* Restore pointer events for all existing events.
Allow pointer events on our new event
*/
for (let e of that.event.parentElement.children) {
e.style.pointerEvents = "";
}
place_in_edit_mode(that.event);
let localevent = that.event;
that.event = null;
callback (localevent);
}
}
}
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.
TODO stop requiring a weird button press to change calendar.
*/
function place_in_edit_mode (event) {
let popup = document.getElementById("popup" + event.id)
let container = popup.getElementsByClassName('dropdown-goes-here')[0]
let calendar_dropdown = document.getElementById('calendar-dropdown-template').firstChild.cloneNode(true);
let [_, calclass] = popup.classList.find(/^CAL_/);
label: {
for (let [i, option] of calendar_dropdown.childNodes.entries()) {
if (option.value === calclass.substr(4)) {
calendar_dropdown.selectedIndex = i;
break label;
}
}
/* no match, try find default calendar */
let t;
if ((t = calendar_dropdown.querySelector("[selected]"))) {
event.properties.calendar = t.value;
}
}
/* Instant change while user is stepping through would be
* preferable. But I believe that