From a64c2a665af1abe0b91f1c5eb1f97df91ed8a4de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Sun, 3 Oct 2021 17:48:13 +0200 Subject: Further work, rework popup. --- static/dragable.js | 6 +- static/globals.js | 309 ++++++++++++--------------------------- static/popup.js | 12 +- static/script.js | 421 +++++++++++++++++++++++++++-------------------------- static/style.scss | 30 ++-- 5 files changed, 330 insertions(+), 448 deletions(-) (limited to 'static') diff --git a/static/dragable.js b/static/dragable.js index 41895760..6eb0b999 100644 --- a/static/dragable.js +++ b/static/dragable.js @@ -21,14 +21,16 @@ function bind_popup_control (nav) { nav.style.cursor = "grabbing"; nav.dataset.grabbed = "true"; nav.dataset.grabPoint = e.clientX + ";" + e.clientY; - let popup = nav.closest(".popup-container"); + // let popup = nav.closest(".popup-container"); + let popup = nav.closest("popup-element"); 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"); + // let popup = nav.closest(".popup-container"); + let popup = nav.closest("popup-element"); popup.style.left = startX + (e.clientX - x) + "px"; popup.style.top = startY + (e.clientY - y) + "px"; diff --git a/static/globals.js b/static/globals.js index 6efba94f..41472264 100644 --- a/static/globals.js +++ b/static/globals.js @@ -1,218 +1,5 @@ "use strict"; -class VEventValue { - constructor (type, value, parameters = {}) { - this.type = type; - this.value = value; - this.parameters = parameters; - } - - to_jcal () { - let value; - let v = this.value; - switch (this.type) { - case 'binary': - /* TOOD */ - break; - case 'date-time': - value = v.format("~Y-~m-~dT~H:~M:~S"); - // TODO TZ - break; - case 'date': - value = v.format("~Y-~m-~d"); - break; - case 'duration': - /* TODO */ - break; - case 'period': - /* TODO */ - break; - case 'utc-offset': - /* TODO */ - break; - case 'recur': - value = v.asJcal(); - break; - - case 'float': - case 'integer': - case 'text': - case 'uri': - case 'cal-address': - case 'boolean': - value = v; - } - return [this.parameters, this.type, value]; - } -} - -/* maybe ... */ -class VEventDuration extends VEventValue { -} - -class VEvent { - constructor (properties = {}, components = []) { - this.properties = properties; - this.components = components; - this.registered = []; - } - - getProperty (key) { - let e = this.properties[key]; - if (! e) return e; - return e.value; - } - - setProperty (key, value) { - let e = this.properties[key]; - if (! e) { - let type = (valid_input_types[key.toUpperCase()] || ['unknown'])[0] - if (typeof type === typeof []) type = type[0]; - e = this.properties[key] = new VEventValue(type, value); - } else { - e.value = value; - } - for (let el of this.registered) { - /* TODO update correct fields, allow component to redraw themselves */ - el.redraw(this); - } - } - - register (htmlNode) { - this.registered.push(htmlNode); - } - - to_jcal () { - let out_properties = [] - for (let [key, value] of Object.entries(this.properties)) { - let sub = value.to_jcal(); - sub.unshift(key) - out_properties.push(sub); - } - return ['vevent', out_properties, [/* alarms go here*/]] - } -} - -function make_vevent_value (value_tag) { - /* TODO parameters */ - return new VEventValue (value_tag.tagName, make_vevent_value_ (value_tag)); -} - -function make_vevent_value_ (value_tag) { - /* RFC6321 3.6. */ - switch (value_tag.tagName) { - case 'binary': - /* Base64 to binary - Seems to handle inline whitespace, which xCal standard reqires - */ - return atob(value_tag.innerHTML) - break; - - case 'boolean': - switch (value_tag.innerHTML) { - case 'true': return true; - case 'false': return false; - default: - console.warn(`Bad boolean ${value_tag.innerHTML}, defaulting with !!`) - return !! value_tag.innerHTML; - } - break; - - case 'time': - case 'date': - case 'date-time': - return parseDate(value_tag.innerHTML); - break; - - case 'duration': - /* TODO duration parser here 'P1D' */ - return value_tag.innerHTML; - break; - - case 'float': - case 'integer': - return +value_tag.innerHTML; - break; - - case 'period': - /* TODO has sub components, meaning that a string wont do */ - let start = value_tag.getElementsByTagName('start')[0] - parseDate(start.innerHTML); - let other; - if ((other = value_tag.getElementsByTagName('end')[0])) { - return parseDate(other.innerHTML) - } else if ((other = value_tag.getElementsByTagName('duration')[0])) { - /* TODO parse duration */ - return other.innerHTML - } else { - console.warn('Invalid end to period, defaulting to 1H'); - return new Date(3600); - } - - case 'recur': - /* TODO parse */ - return ""; - - case 'uc-offset': - /* TODO parse */ - return ""; - - default: - console.warn(`Unknown type '${value_tag.tagName}', defaulting to string`) - case 'cal-address': - case 'uri': - case 'text': - return value_tag.innerHTML; - } -} - - -/* xml dom object -> class VEvent */ -function xml_to_vcal (xml) { - /* xml MUST have a VEVENT (or equivalent) as its root */ - let properties = xml.getElementsByTagName('properties')[0]; - let components = xml.getElementsByTagName('components')[0]; - - let property_map = {} - if (properties) { - for (var i = 0; i < properties.childElementCount; i++) { - let tag = properties.childNodes[i]; - let parameters = {}; - let value = []; - for (var j = 0; j < tag.childElementCount; j++) { - let child = tag.childNodes[j]; - switch (tag.tagName) { - case 'parameters': - parameters = /* handle parameters */ {}; - break; - - /* These can contain multiple value tags, per - RFC6321 3.4.1.1. */ - case 'categories': - case 'resources': - case 'freebusy': - case 'exdate': - case 'rdate': - value.push(make_vevent_value(child)); - break; - default: - value = make_vevent_value(child); - } - } - property_map[tag.tagName] = value; - } - } - - let component_list = [] - if (components) { - for (let child of components.childNodes) { - component_list.push(xml_to_vcal(child)) - } - } - - return new VEvent(property_map, component_list) -} - const vcal_objects = {}; class ComponentVEvent extends HTMLElement { @@ -226,6 +13,13 @@ class ComponentVEvent extends HTMLElement { should take care of that some other way */ } + connectedCallback () { + let uid; + if ((uid = this.dataset.uid)) { + this.redraw (vcal_objects[uid]); + } + } + redraw (data) { // update ourselves from template @@ -270,7 +64,14 @@ class ComponentEdit extends ComponentVEvent { /* Edit tab is rendered here. It's left blank server-side, since it only makes sense to have something here if we have javascript */ - this.redraw(vcal_objects[this.dataset.uid]); + + let data = vcal_objects[this.dataset.uid] + + if (! data) { + throw `Data missing for uid ${this.dataset.uid}.` + } + + this.redraw(data); for (let el of this.getElementsByClassName("interactive")) { el.addEventListener('input', () => { @@ -320,9 +121,31 @@ class ComponentEdit extends ComponentVEvent { } +function find_popup (uid) { + for (let el of vcal_objects[uid].registered) { + if (el.tagName === 'popup-element') { + return el; + } + } + throw 'Popup not fonud'; +} + +function find_block (uid) { + for (let el of vcal_objects[uid].registered) { + if (el.tagName === 'vevent-block') { + return el; + } + } + throw 'Popup not fonud'; +} + class ComponentBlock extends ComponentVEvent { constructor () { super(); + + this.addEventListener('click', () => { + toggle_popup(find_popup(this.dataset.uid)); + }); } redraw (data) { @@ -330,13 +153,13 @@ class ComponentBlock extends ComponentVEvent { let p; if ((p = data.getProperty('dtstart'))) { - this.style.top = date_to_percent(p, 1) + "%"; - console.log('dtstart', p); + this.style.top = date_to_percent(to_local(p), 1) + "%"; + // console.log('dtstart', p); } if ((p = data.getProperty('dtend'))) { this.style.height = 'unset'; - console.log('dtend', p); - this.style.bottom = (100 - date_to_percent(p, 1)) + "%"; + // console.log('dtend', p); + this.style.bottom = (100 - date_to_percent(to_local(p), 1)) + "%"; } } } @@ -439,6 +262,56 @@ class DateTimeInput extends HTMLElement { customElements.define('date-time-input', DateTimeInput) +class PopupElement extends HTMLElement { + constructor () { + super(); + + /* TODO populate remaining */ + // this.id = 'popup' + this.dataset.uid + } + + redraw () { + console.log('IMPLEMENT ME'); + } + + connectedCallback() { + let body = document.getElementById('popup-template').content.cloneNode(true).firstElementChild; + + let uid = this.dataset.uid + // console.log(uid); + + body.getElementsByClassName('populate-with-uid') + .forEach((e) => e.setAttribute('data-uid', uid)); + + /* tabs */ + let tabgroup_id = gensym(); + for (let tab of body.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); + } + /* end tabs */ + + /* nav bar */ + let nav = body.getElementsByClassName("popup-control")[0]; + bind_popup_control(nav); + + let btn = body.querySelector('.popup-control .close-tooltip') + btn.addEventListener('click', () => { + close_popup(this); + }); + /* end nav bar */ + + this.replaceChildren(body); + } +} + +window.addEventListener('load', function () { + customElements.define('popup-element', PopupElement) +}); + function wholeday_checkbox (box) { box.closest('.timeinput') .getElementsByTagName('date-time-input') diff --git a/static/popup.js b/static/popup.js index e19db6f2..0b04b280 100644 --- a/static/popup.js +++ b/static/popup.js @@ -2,12 +2,14 @@ /* event component => coresponding popup component */ function event_from_popup(popup) { - return document.getElementById(popup.id.substr(5)) + // return document.getElementById(popup.id.substr(5)) + return find_block(popup.closest('[data-uid]').dataset.uid) } /* popup component => coresponding event component */ function popup_from_event(event) { - return document.getElementById("popup" + event.id); + // return document.getElementById("popup" + event.id); + return find_popup(event.closest('[data-uid]').dataset.uid) } /* hides given popup */ @@ -17,7 +19,7 @@ function close_popup(popup) { /* hides all popups */ function close_all_popups () { - for (let popup of document.querySelectorAll(".popup-container.visible")) { + for (let popup of document.querySelectorAll("popup-element.visible")) { close_popup(popup); } } @@ -51,8 +53,8 @@ function open_popup(popup) { } /* toggles open/closed status of popup given by id */ -function toggle_popup(popup_id) { - let popup = document.getElementById(popup_id); +function toggle_popup(popup) { + // let popup = document.getElementById(popup_id); if (popup.classList.contains("visible")) { close_popup(popup); } else { diff --git a/static/script.js b/static/script.js index b4fb7bda..5ef498f3 100644 --- a/static/script.js +++ b/static/script.js @@ -4,211 +4,216 @@ calp specific stuff */ -// 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 () { -// /* TODO this doesn't clone JS attributes */ -// -// let event = document.getElementById("event-template") -// .firstChild.cloneNode(true); -// let popup = document.getElementById("popup-template") -// .firstChild.cloneNode(true); -// -// /* -------------------- */ -// /* Manually transfer or recreate attributes which we still need */ -// /* TODO shouldn't these use transferListeners (or similar)? -// See input_list.js:transferListeners */ -// -// for (let dt of popup.getElementsByClassName("date-time")) { -// init_date_time_single(dt); -// } -// -// popup.getElementsByClassName("edit-form")[0].onsubmit = function () { -// create_event(event); -// return false; /* stop default */ -// } -// -// /* -------------------- */ -// /* Fix tabs for newly created popup */ -// -// 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 fractionsb -// 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'. */ -// new VComponent( -// event, -// wide_element=wide_element); -// // bind_properties(event, wide_element); -// -// /* requires that dtstart and dtend properties are initialized */ -// -// /* ---------------------------------------- */ -// -// /* 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) -// -// // console.log(that.event); -// console.log(d1, d2); -// 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); -// -// } -// } -// } +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 () { + /* TODO this doesn't clone JS attributes */ + + // let event = document.getElementById("event-template") + // .firstChild.cloneNode(true); + // let popup = document.getElementById("popup-template") + // .firstChild.cloneNode(true); + + // document.createElement('vevent-block'); + + /* -------------------- */ + /* Manually transfer or recreate attributes which we still need */ + /* TODO shouldn't these use transferListeners (or similar)? + See input_list.js:transferListeners */ + + for (let dt of popup.getElementsByClassName("date-time")) { + init_date_time_single(dt); + } + + popup.getElementsByClassName("edit-form")[0].onsubmit = function () { + create_event(event); + return false; /* stop default */ + } + + /* -------------------- */ + /* Fix tabs for newly created popup */ + + 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 fractionsb + 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; + that.event = document.createElement('vevent-block'); + + /* 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'. */ + // new VComponent( + // event, + // wide_element=wide_element); + // bind_properties(event, wide_element); + + /* requires that dtstart and dtend properties are initialized */ + + /* ---------------------------------------- */ + + /* 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) + + // console.log(that.event); + console.log(d1, d2); + 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); + + } + } +} @@ -316,9 +321,9 @@ window.addEventListener('load', function () { } } - for (let nav of document.getElementsByClassName("popup-control")) { - bind_popup_control(nav); - } + // for (let nav of document.getElementsByClassName("popup-control")) { + // bind_popup_control(nav); + // } for (let el of document.getElementsByClassName("event")) { /* Popup script replaces need for anchors to events. diff --git a/static/style.scss b/static/style.scss index a29bb24b..4b4f573b 100644 --- a/static/style.scss +++ b/static/style.scss @@ -744,6 +744,21 @@ along with their colors. ---------------------------------------- */ +popup-element { + display: none; + position: absolute; + z-index: 1000; + + /* ??? */ + left: 10px; + top: -50px; + + box-shadow: gray 10px 10px 10px; + + &.visible { + display: block; + } +} .popup { display: flex; @@ -757,21 +772,6 @@ along with their colors. min-width: 60ch; min-height: 30ch; - &-container { - display: none; - position: absolute; - z-index: 1000; - - /* ??? */ - left: 10px; - top: -50px; - - box-shadow: gray 10px 10px 10px; - - &.visible { - display: block; - } - } input { white-space: initial; -- cgit v1.2.3