'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(); 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 = ""; }); } /* * 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 */ 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"; } 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 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(); /* servere 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