From c6c65f9e8273a5bc1b2ac1155d66003d2b98591c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Mon, 4 Oct 2021 17:40:59 +0200 Subject: {.js => .ts} on relavant files. --- static/globals.ts | 337 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 337 insertions(+) create mode 100644 static/globals.ts (limited to 'static/globals.ts') diff --git a/static/globals.ts b/static/globals.ts new file mode 100644 index 00000000..86368e9a --- /dev/null +++ b/static/globals.ts @@ -0,0 +1,337 @@ +"use strict"; + +const vcal_objects = {}; + +class ComponentVEvent extends HTMLElement { + constructor () { + super (); + this.template = document.getElementById(this.tagName); + + let uid; + if ((uid = this.dataset.uid)) { + vcal_objects[uid].register(this); + } + + /* We DON'T have a redraw here in the general case, since the + HTML rendered server-side should be fine enough for us. + Those that need a direct rerendering (such as the edit tabs) + 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 + + if (! this.template) { + throw "Something"; + } + + let body = this.template.content.cloneNode(true).firstElementChild; + + for (let el of body.getElementsByClassName("bind")) { + let p = el.dataset.property; + let d, fmt; + if ((d = data.getProperty(p))) { + if ((fmt = el.dataset.fmt)) { + el.innerHTML = d.format(fmt); + } else { + el.innerHTML = d; + } + } + } + + this.replaceChildren(body); + } + +} + +class ComponentDescription extends ComponentVEvent { + constructor () { + super() ; + } + +} + +class ComponentEdit extends ComponentVEvent { + constructor () { + super(); + + this.firstTime = true; + } + + connectedCallback() { + + /* Edit tab is rendered here. It's left blank server-side, since + it only makes sense to have something here if we have javascript */ + + 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', () => { + vcal_objects[this.dataset.uid].setProperty( + el.dataset.property, + el.value) + }); + } + } + + redraw (data) { + // update ourselves from template + + if (! this.template) { + throw "Something"; + } + + let body; + if (this.firstTime) { + body = this.template.content.cloneNode(true).firstElementChild; + } else { + body = this; + } + + for (let el of body.getElementsByClassName("interactive")) { + let p = el.dataset.property; + let d; + if ((d = data.getProperty(p))) { + /* + https://stackoverflow.com/questions/57157830/how-can-i-specify-the-sequence-of-running-nested-web-components-constructors + */ + window.setTimeout (() => { + /* NOTE Some specific types might require special formatting + here. But due to my custom components implementing custom + `.value' procedures, we might not need any special cases + here */ + el.value = d; + }); + } + } + + if (this.firstTime) { + this.replaceChildren(body); + this.firstTime = false; + } + } + +} + +function find_popup (uid) { + // for (let el of vcal_objects[uid].registered) { + // if (el.tagName === 'popup-element') { + // return el; + // } + // } + // throw 'Popup not fonud'; + return document.querySelector(`popup-element[data-uid="${uid}"]`) +} + +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) { + super.redraw(data); + + let p; + if ((p = data.getProperty('dtstart'))) { + 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(to_local(p), 1)) + "%"; + } + } +} + +window.addEventListener('load', function () { + + // let json_objects_el = document.getElementById('json-objects'); + let div = document.getElementById('xcal-data'); + let vevents = div.firstElementChild.childNodes; + + for (let vevent of vevents) { + let ev = xml_to_vcal(vevent); + vcal_objects[ev.getProperty('uid')] = ev + } + + /* + - .popup + - .block + - .list + */ + /* + let vevent_els = document.getElementsByClassName('vevent') + for (let el of vevent_els) { + try { + vcal_objects[el.dataset.uid].register(el); + } catch { + console.error("Invalid something, uid = ", el.dataset.uid, + "el = ", el + ); + } + } + */ + + customElements.define('vevent-description', ComponentDescription); + customElements.define('vevent-edit', ComponentEdit); + customElements.define('vevent-block', ComponentBlock); +}) + + + +class DateTimeInput extends HTMLElement { + constructor () { + super(); + this.innerHTML = '' + } + + static get observedAttributes () { + return [ 'dateonly' ] + } + + attributeChangedCallback (name, from, to) { + console.log(this, name, boolean(from), boolean(to)); + switch (name) { + case 'dateonly': + this.querySelector('[type="time"]').disabled = boolean(to) + break; + } + } + + get dateonly () { + return boolean(this.getAttribute('dateonly')); + } + + set dateonly (bool) { + this.setAttribute ('dateonly', bool); + } + + get value () { + + let dt; + let date = this.querySelector("[type='date']").value; + if (boolean(this.getAttribute('dateonly'))) { + dt = parseDate(date); + dt.type = 'date'; + } else { + let time = this.querySelector("[type='time']").value; + dt = parseDate(date + 'T' + time) + dt.type = 'date-time'; + } + return dt; + } + + set value (new_value) { + let date, time; + if (new_value instanceof Date) { + date = new_value.format("~L~Y-~m-~d"); + time = new_value.format("~L~H:~M:~S"); + } else { + [date, time] = new_value.split('T') + } + this.querySelector("[type='date']").value = date; + this.querySelector("[type='time']").value = time; + } + + addEventListener(type, proc) { + if (type != 'input') throw "Only input supported"; + + this.querySelector("[type='date']").addEventListener(type, proc); + this.querySelector("[type='time']").addEventListener(type, proc); + } +} + +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); + + let that = this; + this.getElementsByClassName("calendar-selection") + .addEventListener('change', function () { + let uid = that.closest('[data-uid]').dataset.uid + let obj = vcal_objects[uid] + this.value; + // event.properties.calendar = this.value; + }); + + } +} + +window.addEventListener('load', function () { + customElements.define('popup-element', PopupElement) +}); + +function wholeday_checkbox (box) { + box.closest('.timeinput') + .getElementsByTagName('date-time-input') + .forEach(el => el.dateonly = box.checked); +} -- cgit v1.2.3 From 8ec2f441d40ab89b40cc3158f65c914eff497cee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Mon, 4 Oct 2021 23:18:24 +0200 Subject: Major typescript work. --- static/globals.ts | 211 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 128 insertions(+), 83 deletions(-) (limited to 'static/globals.ts') diff --git a/static/globals.ts b/static/globals.ts index 86368e9a..64a3613f 100644 --- a/static/globals.ts +++ b/static/globals.ts @@ -1,15 +1,18 @@ "use strict"; -const vcal_objects = {}; +const vcal_objects: Map = new Map() class ComponentVEvent extends HTMLElement { - constructor () { - super (); - this.template = document.getElementById(this.tagName); + + template: HTMLTemplateElement + + constructor() { + super(); + this.template = document.getElementById(this.tagName) as HTMLTemplateElement; let uid; if ((uid = this.dataset.uid)) { - vcal_objects[uid].register(this); + vcal_objects.get(uid)?.register(this); } /* We DON'T have a redraw here in the general case, since the @@ -18,24 +21,26 @@ class ComponentVEvent extends HTMLElement { should take care of that some other way */ } - connectedCallback () { - let uid; + connectedCallback() { + let uid, v; if ((uid = this.dataset.uid)) { - this.redraw (vcal_objects[uid]); + v = vcal_objects.get(uid) + if (v) this.redraw(v); } } - redraw (data) { + redraw(data: VEvent) { // update ourselves from template - if (! this.template) { + if (!this.template) { throw "Something"; } - let body = this.template.content.cloneNode(true).firstElementChild; + let body = (this.template.content.cloneNode(true) as DocumentFragment).firstElementChild!; for (let el of body.getElementsByClassName("bind")) { - let p = el.dataset.property; + if (!(el instanceof HTMLElement)) continue; + let p = el.dataset.property!; let d, fmt; if ((d = data.getProperty(p))) { if ((fmt = el.dataset.fmt)) { @@ -52,17 +57,27 @@ class ComponentVEvent extends HTMLElement { } class ComponentDescription extends ComponentVEvent { - constructor () { - super() ; + constructor() { + super(); } } class ComponentEdit extends ComponentVEvent { - constructor () { + + firstTime: boolean + uid: string + + constructor() { super(); this.firstTime = true; + + if (this.dataset.uid === undefined) { + throw "data-uid must be set" + } else { + this.uid = this.dataset.uid; + } } connectedCallback() { @@ -70,50 +85,59 @@ 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 */ - let data = vcal_objects[this.dataset.uid] + let data = vcal_objects.get(this.uid) - if (! data) { + if (!data) { throw `Data missing for uid ${this.dataset.uid}.` } this.redraw(data); + return; + for (let el of this.getElementsByClassName("interactive")) { el.addEventListener('input', () => { - vcal_objects[this.dataset.uid].setProperty( - el.dataset.property, + let obj = vcal_objects.get(this.uid) + if (obj === undefined) { + throw 'No object with uid ' + this.uid + } + if (!(el instanceof HTMLInputElement)) return; + obj.setProperty( + el.dataset.property!, el.value) }); } } - redraw (data) { + redraw(data: VEvent) { // update ourselves from template - if (! this.template) { + if (!this.template) { throw "Something"; } let body; if (this.firstTime) { - body = this.template.content.cloneNode(true).firstElementChild; + body = (this.template.content.cloneNode(true) as DocumentFragment).firstElementChild!; } else { body = this; } for (let el of body.getElementsByClassName("interactive")) { - let p = el.dataset.property; - let d; + if (!(el instanceof HTMLInputElement)) continue; + let p = el.dataset.property!; + let d: any; if ((d = data.getProperty(p))) { /* https://stackoverflow.com/questions/57157830/how-can-i-specify-the-sequence-of-running-nested-web-components-constructors */ - window.setTimeout (() => { + window.setTimeout(() => { /* NOTE Some specific types might require special formatting here. But due to my custom components implementing custom `.value' procedures, we might not need any special cases here */ - el.value = d; + console.log(el, d); + (el as HTMLInputElement).value = d; }); } } @@ -126,59 +150,68 @@ class ComponentEdit extends ComponentVEvent { } -function find_popup (uid) { +function find_popup(uid: uid): HTMLElement | null { // for (let el of vcal_objects[uid].registered) { // if (el.tagName === 'popup-element') { // return el; // } // } // throw 'Popup not fonud'; - return document.querySelector(`popup-element[data-uid="${uid}"]`) + return document.querySelector(`popup-element[data-uid="${uid}"]`) as HTMLElement } -function find_block (uid) { - for (let el of vcal_objects[uid].registered) { +function find_block(uid: uid): HTMLElement | null { + let obj = vcal_objects.get(uid) + if (obj === undefined) { + return null; + } + for (let el of obj.registered) { if (el.tagName === 'vevent-block') { return el; } } - throw 'Popup not fonud'; + // throw 'Popup not fonud'; + return null; } class ComponentBlock extends ComponentVEvent { - constructor () { + constructor() { super(); this.addEventListener('click', () => { - toggle_popup(find_popup(this.dataset.uid)); + let uid = this.dataset.uid + if (uid === undefined) throw new Error('UID missing from' + this) + let popup = find_popup(uid); + if (popup === null) throw new Error('no popup for uid ' + uid); + toggle_popup(popup); }); } - redraw (data) { + redraw(data: VEvent) { super.redraw(data); let p; if ((p = data.getProperty('dtstart'))) { - this.style.top = date_to_percent(to_local(p), 1) + "%"; + this.style.top = date_to_percent(to_local(p)) + "%"; // console.log('dtstart', p); } if ((p = data.getProperty('dtend'))) { this.style.height = 'unset'; // console.log('dtend', p); - this.style.bottom = (100 - date_to_percent(to_local(p), 1)) + "%"; + this.style.bottom = (100 - date_to_percent(to_local(p))) + "%"; } } } -window.addEventListener('load', function () { +window.addEventListener('load', function() { // let json_objects_el = document.getElementById('json-objects'); - let div = document.getElementById('xcal-data'); - let vevents = div.firstElementChild.childNodes; + let div = document.getElementById('xcal-data')!; + let vevents = div.firstElementChild!.children; for (let vevent of vevents) { let ev = xml_to_vcal(vevent); - vcal_objects[ev.getProperty('uid')] = ev + vcal_objects.set(ev.getProperty('uid'), ev) } /* @@ -206,49 +239,56 @@ window.addEventListener('load', function () { -class DateTimeInput extends HTMLElement { - constructor () { + +class DateTimeInput extends /* HTMLInputElement */ HTMLElement { + constructor() { super(); this.innerHTML = '' + console.log('constructing datetime input') } - static get observedAttributes () { - return [ 'dateonly' ] + static get observedAttributes() { + return ['dateonly'] } - attributeChangedCallback (name, from, to) { + attributeChangedCallback(name: string, from: any, to: any) { console.log(this, name, boolean(from), boolean(to)); switch (name) { - case 'dateonly': - this.querySelector('[type="time"]').disabled = boolean(to) - break; + case 'dateonly': + (this.querySelector('input[type="time"]') as HTMLInputElement) + .disabled = boolean(to) + break; } } - get dateonly () { + get dateonly(): boolean { return boolean(this.getAttribute('dateonly')); } - set dateonly (bool) { - this.setAttribute ('dateonly', bool); + set dateonly(bool: boolean) { + this.setAttribute('dateonly', "" + bool); } - get value () { - + get valueAsDate(): Date { let dt; - let date = this.querySelector("[type='date']").value; + let date = (this.querySelector("input[type='date']") as HTMLInputElement).value; if (boolean(this.getAttribute('dateonly'))) { dt = parseDate(date); dt.type = 'date'; } else { - let time = this.querySelector("[type='time']").value; + let time = (this.querySelector("input[type='time']") as HTMLInputElement).value; dt = parseDate(date + 'T' + time) dt.type = 'date-time'; } return dt; } - set value (new_value) { + get value(): string { + return this.valueAsDate.format("~Y-~m-~dT~H:~M:~S") + } + + set value(new_value: Date | string) { + console.log('Setting date'); let date, time; if (new_value instanceof Date) { date = new_value.format("~L~Y-~m-~d"); @@ -256,36 +296,42 @@ class DateTimeInput extends HTMLElement { } else { [date, time] = new_value.split('T') } - this.querySelector("[type='date']").value = date; - this.querySelector("[type='time']").value = time; + (this.querySelector("input[type='date']") as HTMLInputElement).value = date; + (this.querySelector("input[type='time']") as HTMLInputElement).value = time; } - addEventListener(type, proc) { + addEventListener(type: string, proc: ((e: Event) => void)) { if (type != 'input') throw "Only input supported"; - this.querySelector("[type='date']").addEventListener(type, proc); - this.querySelector("[type='time']").addEventListener(type, proc); + (this.querySelector("input[type='date']") as HTMLInputElement) + .addEventListener(type, proc); + (this.querySelector("input[type='time']") as HTMLInputElement) + .addEventListener(type, proc); } } -customElements.define('date-time-input', DateTimeInput) +customElements.define('date-time-input', DateTimeInput /*, { extends: 'input' } */) class PopupElement extends HTMLElement { - constructor () { + constructor() { super(); /* TODO populate remaining */ // this.id = 'popup' + this.dataset.uid } - redraw () { + redraw() { console.log('IMPLEMENT ME'); } connectedCallback() { - let body = document.getElementById('popup-template').content.cloneNode(true).firstElementChild; + let template: HTMLTemplateElement = document.getElementById('popup-template') as HTMLTemplateElement + let body = (template.content.cloneNode(true) as DocumentFragment).firstElementChild!; - let uid = this.dataset.uid + if (this.dataset.uid === null) { + throw 'UID is required' + } + let uid = this.dataset.uid! // console.log(uid); body.getElementsByClassName('populate-with-uid') @@ -295,43 +341,42 @@ class PopupElement extends HTMLElement { let tabgroup_id = gensym(); for (let tab of body.querySelectorAll(".tabgroup .tab")) { let new_id = gensym(); - let input = tab.querySelector("input"); + let input = tab.querySelector("input")!; input.id = new_id; input.name = tabgroup_id; - tab.querySelector("label").setAttribute('for', new_id); + tab.querySelector("label")!.setAttribute('for', new_id); } /* end tabs */ /* nav bar */ - let nav = body.getElementsByClassName("popup-control")[0]; + let nav = body.getElementsByClassName("popup-control")[0] as HTMLElement; bind_popup_control(nav); - let btn = body.querySelector('.popup-control .close-tooltip') - btn.addEventListener('click', () => { - close_popup(this); - }); + let btn = body.querySelector('.popup-control .close-tooltip') as HTMLButtonElement + btn.addEventListener('click', () => close_popup(this)); /* end nav bar */ this.replaceChildren(body); let that = this; - this.getElementsByClassName("calendar-selection") - .addEventListener('change', function () { - let uid = that.closest('[data-uid]').dataset.uid - let obj = vcal_objects[uid] - this.value; + this.getElementsByClassName("calendar-selection")[0] + .addEventListener('change', function() { + let uid = (that.closest('[data-uid]') as HTMLElement).dataset.uid! + let obj = vcal_objects.get(uid) + // TODO this procedure + // this.value; // event.properties.calendar = this.value; }); } } -window.addEventListener('load', function () { +window.addEventListener('load', function() { customElements.define('popup-element', PopupElement) }); -function wholeday_checkbox (box) { - box.closest('.timeinput') - .getElementsByTagName('date-time-input') - .forEach(el => el.dateonly = box.checked); +function wholeday_checkbox(box: HTMLInputElement) { + box.closest('.timeinput')! + .querySelectorAll('input[is="date-time"]') + .forEach((el) => { (el as DateTimeInput).dateonly = box.checked }); } -- cgit v1.2.3 From 612948090468fbc6279d7c9774d4f3839ab54d72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Wed, 6 Oct 2021 02:17:07 +0200 Subject: Fix issue with datetimes not getting set. --- static/globals.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'static/globals.ts') diff --git a/static/globals.ts b/static/globals.ts index 64a3613f..0df1aabd 100644 --- a/static/globals.ts +++ b/static/globals.ts @@ -124,7 +124,7 @@ class ComponentEdit extends ComponentVEvent { } for (let el of body.getElementsByClassName("interactive")) { - if (!(el instanceof HTMLInputElement)) continue; + if (!(el instanceof HTMLElement)) continue; let p = el.dataset.property!; let d: any; if ((d = data.getProperty(p))) { @@ -136,7 +136,9 @@ class ComponentEdit extends ComponentVEvent { here. But due to my custom components implementing custom `.value' procedures, we might not need any special cases here */ - console.log(el, d); + /* Technically we just want to cast to HTMLElement with + value field here, but multiple types implement it + sepparately, and no common interface exist */ (el as HTMLInputElement).value = d; }); } -- cgit v1.2.3 From 4f499ccbf71d0ae662159515bb568826e72678a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Wed, 6 Oct 2021 04:32:11 +0200 Subject: Drive popup tabs through javascript. --- static/globals.ts | 120 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 108 insertions(+), 12 deletions(-) (limited to 'static/globals.ts') diff --git a/static/globals.ts b/static/globals.ts index 0df1aabd..970de8f7 100644 --- a/static/globals.ts +++ b/static/globals.ts @@ -63,6 +63,18 @@ class ComponentDescription extends ComponentVEvent { } +function popuplateTab(tab: HTMLElement, tabgroup: string, index: number) { + // console.log(tab); + let new_id = gensym(); + let input = tab.querySelector('input[type="radio"]') as HTMLInputElement; + let label = tab.querySelector("label")! + tab.style.setProperty('--tab-index', '' + index); + /* TODO this throws a number of errors, but somehow still works...? */ + input.name = tabgroup + input.id = new_id; + label.setAttribute('for', new_id); +} + class ComponentEdit extends ComponentVEvent { firstTime: boolean @@ -246,7 +258,7 @@ class DateTimeInput extends /* HTMLInputElement */ HTMLElement { constructor() { super(); this.innerHTML = '' - console.log('constructing datetime input') + // console.log('constructing datetime input') } static get observedAttributes() { @@ -254,7 +266,7 @@ class DateTimeInput extends /* HTMLInputElement */ HTMLElement { } attributeChangedCallback(name: string, from: any, to: any) { - console.log(this, name, boolean(from), boolean(to)); + // console.log(this, name, boolean(from), boolean(to)); switch (name) { case 'dateonly': (this.querySelector('input[type="time"]') as HTMLInputElement) @@ -290,7 +302,7 @@ class DateTimeInput extends /* HTMLInputElement */ HTMLElement { } set value(new_value: Date | string) { - console.log('Setting date'); + // console.log('Setting date'); let date, time; if (new_value instanceof Date) { date = new_value.format("~L~Y-~m-~d"); @@ -314,16 +326,67 @@ class DateTimeInput extends /* HTMLInputElement */ HTMLElement { customElements.define('date-time-input', DateTimeInput /*, { extends: 'input' } */) +function verifySlot(el: Node | null): el is HTMLElement { + if (el === null) { + console.error("Element is null"); + return false; + } + if (!(el instanceof HTMLElement)) { + console.error("Node is not an HTMLElement", el); + return false; + } + return true +} + +class TabElement extends HTMLElement { + constructor() { + super(); + } + + connectedCallback() { + // this.replaceChildren(template.cloneNode(true)); + let template + = (document.getElementById('tab-template') as HTMLTemplateElement) + .content + // const shadowRoot = this.attachShadow({ mode: 'open' }) + // .appendChild(template.cloneNode(true)); + // console.log(this); + let label = this.querySelector('[slot="label"]') + let content = this.querySelector('[slot="content"]') + if (!verifySlot(label)) throw "Bad label"; + if (!verifySlot(content)) throw "Bad content"; + + this.replaceChildren(template.cloneNode(true)); + this.querySelector('slot[name="label"]')!.replaceWith(label); + this.querySelector('slot[name="content"]')!.replaceWith(content); + } +} + +function buildDescriptionList(data: [string, any][]): HTMLElement { + let dl = document.createElement('dl'); + for (let [key, val] of data) { + dl.appendChild(makeElement('dt', { innerText: key })) + dl.appendChild(makeElement('dd', { innerText: val })) + } + return dl; +} + class PopupElement extends HTMLElement { + + tabgroup_id: string + tabcount: number + constructor() { super(); /* TODO populate remaining */ // this.id = 'popup' + this.dataset.uid + this.tabgroup_id = gensym(); + this.tabcount = 0 } redraw() { - console.log('IMPLEMENT ME'); + console.warn('IMPLEMENT ME'); } connectedCallback() { @@ -340,14 +403,23 @@ class PopupElement extends HTMLElement { .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); - } + // for (let tab of body.querySelectorAll(".tabgroup .tab")) { + // } + window.setTimeout(() => { + // let tabs = this.querySelector('tab-element')! + // .shadowRoot! + // .querySelectorAll('label') + // console.log(tabs); + // console.log(this.getElementsByTagName('tab-element')) + for (let tab of this.getElementsByTagName('tab-element')) { + // console.log(tab_container); + // let tab = tab_container.shadowRoot!; + // tab.documentElement.style.setProperty('--i', i); + popuplateTab(tab as TabElement, this.tabgroup_id, this.tabcount) + this.tabcount += 1 + } + (this.querySelector('tab-element label') as HTMLInputElement).click() + }); /* end tabs */ /* nav bar */ @@ -370,11 +442,35 @@ class PopupElement extends HTMLElement { // event.properties.calendar = this.value; }); + + + let tab = makeElement('tab-element', { title: 'Debug' }) as TabElement + /// let tab = new TabElement(); + tab.setAttribute('title', 'Debug') + tab.appendChild(makeElement('span', { slot: 'label', innerText: 'D' })) + // let dl = makeElement('dl', { slot: 'content' }) + let obj = vcal_objects.get(uid)! + let dl = buildDescriptionList( + Array.from(obj.boundProperties) + .map(key => [key, obj.getProperty(key)])) + dl.slot = 'content' + tab.appendChild(dl) + + this.addTab(tab); + // window.setTimeout(() => { this.addTab(tab) }) + } + + addTab(tab: TabElement) { + let tabgroup = this.getElementsByClassName('tabgroup')![0]! + tabgroup.append(tab); + popuplateTab(tab, this.tabgroup_id, this.tabcount) + this.tabcount += 1 } } window.addEventListener('load', function() { customElements.define('popup-element', PopupElement) + customElements.define('tab-element', TabElement) }); function wholeday_checkbox(box: HTMLInputElement) { -- cgit v1.2.3 From 200c0bc92203f2103805f1d09602b02800a8593a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Sun, 10 Oct 2021 15:13:46 +0200 Subject: Mostly fix datetime values in frontend. --- static/globals.ts | 57 ++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 9 deletions(-) (limited to 'static/globals.ts') diff --git a/static/globals.ts b/static/globals.ts index 970de8f7..dabd6d8c 100644 --- a/static/globals.ts +++ b/static/globals.ts @@ -2,6 +2,14 @@ const vcal_objects: Map = new Map() +interface HasValue { + value: string +} + +function hasValue(obj: any): obj is HasValue { + return 'value' in obj; +} + class ComponentVEvent extends HTMLElement { template: HTMLTemplateElement @@ -70,9 +78,13 @@ function popuplateTab(tab: HTMLElement, tabgroup: string, index: number) { let label = tab.querySelector("label")! tab.style.setProperty('--tab-index', '' + index); /* TODO this throws a number of errors, but somehow still works...? */ - input.name = tabgroup - input.id = new_id; - label.setAttribute('for', new_id); + if (input !== null) { + input.name = tabgroup + input.id = new_id; + } + if (label !== null) { + label.setAttribute('for', new_id); + } } class ComponentEdit extends ComponentVEvent { @@ -105,15 +117,19 @@ class ComponentEdit extends ComponentVEvent { this.redraw(data); - return; + // return; for (let el of this.getElementsByClassName("interactive")) { + // console.log(el); el.addEventListener('input', () => { let obj = vcal_objects.get(this.uid) if (obj === undefined) { throw 'No object with uid ' + this.uid } - if (!(el instanceof HTMLInputElement)) return; + if (!(hasValue(el) && el instanceof HTMLElement)) { + console.log(el, 'not an HTMLInputElement'); + return; + } obj.setProperty( el.dataset.property!, el.value) @@ -204,15 +220,36 @@ class ComponentBlock extends ComponentVEvent { redraw(data: VEvent) { super.redraw(data); + let p; if ((p = data.getProperty('dtstart'))) { - this.style.top = date_to_percent(to_local(p)) + "%"; + let c = this.closest('.event-container') as HTMLElement + let start = parseDate(c.dataset.start!).getTime() + let end = parseDate(c.dataset.end!).getTime(); + // console.log(p); + let pp = to_local(p).getTime() + let result = 100 * (Math.min(end, Math.max(start, pp)) - start) / (end - start) + "%" + if (c.classList.contains('longevents')) { + this.style.left = result + } else { + this.style.top = result + } // console.log('dtstart', p); } if ((p = data.getProperty('dtend'))) { - this.style.height = 'unset'; // console.log('dtend', p); - this.style.bottom = (100 - date_to_percent(to_local(p))) + "%"; + let c = this.closest('.event-container') as HTMLElement + let start = parseDate(c.dataset.start!).getTime() + let end = parseDate(c.dataset.end!).getTime(); + let pp = to_local(p).getTime() + let result = 100 - (100 * (Math.min(end, Math.max(start, pp)) - start) / (end - start)) + "%" + if (c.classList.contains('longevents')) { + this.style.width = 'unset'; + this.style.right = result; + } else { + this.style.height = 'unset'; + this.style.bottom = result; + } } } } @@ -356,6 +393,8 @@ class TabElement extends HTMLElement { if (!verifySlot(label)) throw "Bad label"; if (!verifySlot(content)) throw "Bad content"; + /* TODO set label hover title somewhere around here */ + this.replaceChildren(template.cloneNode(true)); this.querySelector('slot[name="label"]')!.replaceWith(label); this.querySelector('slot[name="content"]')!.replaceWith(content); @@ -447,7 +486,7 @@ class PopupElement extends HTMLElement { let tab = makeElement('tab-element', { title: 'Debug' }) as TabElement /// let tab = new TabElement(); tab.setAttribute('title', 'Debug') - tab.appendChild(makeElement('span', { slot: 'label', innerText: 'D' })) + tab.appendChild(makeElement('span', { slot: 'label', innerText: "🐸" })) // let dl = makeElement('dl', { slot: 'content' }) let obj = vcal_objects.get(uid)! let dl = buildDescriptionList( -- cgit v1.2.3 From 2d0ec2b162e3e2851fef7f280aab21c9f00cd171 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Sun, 31 Oct 2021 20:48:23 +0100 Subject: Everything but lib. --- static/globals.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) (limited to 'static/globals.ts') diff --git a/static/globals.ts b/static/globals.ts index dabd6d8c..0ca2e3de 100644 --- a/static/globals.ts +++ b/static/globals.ts @@ -1,3 +1,25 @@ +export { + vcal_objects, + hasValue, + ComponentVEvent, + ComponentDescription, + popuplateTab, + ComponentEdit, + find_popup, + find_block, + ComponentBlock, + DateTimeInput, + verifySlot, + TabElement, + buildDescriptionList, + PopupElement, + wholeday_checkbox, +} + +import { close_popup, toggle_popup } from './popup' +import { VEvent, xml_to_vcal } from './vevent' +import { bind_popup_control } from './dragable' + "use strict"; const vcal_objects: Map = new Map() -- cgit v1.2.3 From 0712c416259e4860ff1910c4a5bcd7b37da6b237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Sun, 31 Oct 2021 21:18:37 +0100 Subject: lib. --- static/globals.ts | 1 + 1 file changed, 1 insertion(+) (limited to 'static/globals.ts') diff --git a/static/globals.ts b/static/globals.ts index 0ca2e3de..9045d4a9 100644 --- a/static/globals.ts +++ b/static/globals.ts @@ -19,6 +19,7 @@ export { import { close_popup, toggle_popup } from './popup' import { VEvent, xml_to_vcal } from './vevent' import { bind_popup_control } from './dragable' +import { uid, parseDate, gensym, to_local, boolean, makeElement } from './lib' "use strict"; -- cgit v1.2.3 From 44f8ae5ba2b6b4954d6562aff2f4e704cfa1a966 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Mon, 1 Nov 2021 16:17:46 +0100 Subject: Limit exports to those used by imports. This gives a clearer picture of what is dead code and what isn't. --- static/globals.ts | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) (limited to 'static/globals.ts') diff --git a/static/globals.ts b/static/globals.ts index 9045d4a9..c18808e7 100644 --- a/static/globals.ts +++ b/static/globals.ts @@ -1,19 +1,5 @@ export { - vcal_objects, - hasValue, - ComponentVEvent, - ComponentDescription, - popuplateTab, - ComponentEdit, - find_popup, - find_block, - ComponentBlock, - DateTimeInput, - verifySlot, - TabElement, - buildDescriptionList, - PopupElement, - wholeday_checkbox, + find_block, find_popup, PopupElement } import { close_popup, toggle_popup } from './popup' -- cgit v1.2.3 From 3afc7d26dcca96925be2e4230b4194a9b335af2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Fri, 5 Nov 2021 21:50:18 +0100 Subject: doc updates. --- static/globals.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) (limited to 'static/globals.ts') diff --git a/static/globals.ts b/static/globals.ts index c18808e7..5e97bf39 100644 --- a/static/globals.ts +++ b/static/globals.ts @@ -19,6 +19,11 @@ function hasValue(obj: any): obj is HasValue { return 'value' in obj; } +/* Root component for all events which content is closely linked to a +@code{VEvent} object + +Lacks an accompaning tag, and shouldn't be directly instanciated. +*/ class ComponentVEvent extends HTMLElement { template: HTMLTemplateElement @@ -73,11 +78,14 @@ class ComponentVEvent extends HTMLElement { } + +/* + +*/ class ComponentDescription extends ComponentVEvent { constructor() { super(); } - } function popuplateTab(tab: HTMLElement, tabgroup: string, index: number) { @@ -96,6 +104,7 @@ function popuplateTab(tab: HTMLElement, tabgroup: string, index: number) { } } +/* */ class ComponentEdit extends ComponentVEvent { firstTime: boolean @@ -213,6 +222,10 @@ function find_block(uid: uid): HTMLElement | null { return null; } +/* + + A grahpical block in the week view. +*/ class ComponentBlock extends ComponentVEvent { constructor() { super(); @@ -229,7 +242,6 @@ class ComponentBlock extends ComponentVEvent { redraw(data: VEvent) { super.redraw(data); - let p; if ((p = data.getProperty('dtstart'))) { let c = this.closest('.event-container') as HTMLElement @@ -300,6 +312,7 @@ window.addEventListener('load', function() { +/* '' */ class DateTimeInput extends /* HTMLInputElement */ HTMLElement { constructor() { super(); @@ -372,6 +385,7 @@ class DateTimeInput extends /* HTMLInputElement */ HTMLElement { customElements.define('date-time-input', DateTimeInput /*, { extends: 'input' } */) + function verifySlot(el: Node | null): el is HTMLElement { if (el === null) { console.error("Element is null"); @@ -384,6 +398,8 @@ function verifySlot(el: Node | null): el is HTMLElement { return true } + +/* */ class TabElement extends HTMLElement { constructor() { super(); @@ -419,6 +435,7 @@ function buildDescriptionList(data: [string, any][]): HTMLElement { return dl; } +/* */ class PopupElement extends HTMLElement { tabgroup_id: string -- cgit v1.2.3 From eff2468afffe7571f01cdfa7b7ea774476f8ecc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Mon, 8 Nov 2021 18:33:05 +0100 Subject: All ComponentVEvent requires uid. --- static/globals.ts | 43 +++++++++++++++++++------------------------ 1 file changed, 19 insertions(+), 24 deletions(-) (limited to 'static/globals.ts') diff --git a/static/globals.ts b/static/globals.ts index 5e97bf39..6b332772 100644 --- a/static/globals.ts +++ b/static/globals.ts @@ -7,8 +7,6 @@ import { VEvent, xml_to_vcal } from './vevent' import { bind_popup_control } from './dragable' import { uid, parseDate, gensym, to_local, boolean, makeElement } from './lib' -"use strict"; - const vcal_objects: Map = new Map() interface HasValue { @@ -27,16 +25,24 @@ Lacks an accompaning tag, and shouldn't be directly instanciated. class ComponentVEvent extends HTMLElement { template: HTMLTemplateElement + uid: string - constructor() { + constructor(uid?: string) { super(); this.template = document.getElementById(this.tagName) as HTMLTemplateElement; - let uid; - if ((uid = this.dataset.uid)) { - vcal_objects.get(uid)?.register(this); + let real_uid; + if (this.dataset.uid) uid = this.dataset.uid; + if (uid) real_uid = uid; + + if (!real_uid) { + throw `UID required` } + this.uid = real_uid; + + vcal_objects.get(this.uid)?.register(this); + /* We DON'T have a redraw here in the general case, since the HTML rendered server-side should be fine enough for us. Those that need a direct rerendering (such as the edit tabs) @@ -108,18 +114,11 @@ function popuplateTab(tab: HTMLElement, tabgroup: string, index: number) { class ComponentEdit extends ComponentVEvent { firstTime: boolean - uid: string constructor() { super(); this.firstTime = true; - - if (this.dataset.uid === undefined) { - throw "data-uid must be set" - } else { - this.uid = this.dataset.uid; - } } connectedCallback() { @@ -227,12 +226,11 @@ function find_block(uid: uid): HTMLElement | null { A grahpical block in the week view. */ class ComponentBlock extends ComponentVEvent { - constructor() { - super(); + constructor(uid?: string) { + super(uid); this.addEventListener('click', () => { - let uid = this.dataset.uid - if (uid === undefined) throw new Error('UID missing from' + this) + let uid = this.uid let popup = find_popup(uid); if (popup === null) throw new Error('no popup for uid ' + uid); toggle_popup(popup); @@ -436,13 +434,13 @@ function buildDescriptionList(data: [string, any][]): HTMLElement { } /* */ -class PopupElement extends HTMLElement { +class PopupElement extends ComponentVEvent { tabgroup_id: string tabcount: number - constructor() { - super(); + constructor(uid?: string) { + super(uid); /* TODO populate remaining */ // this.id = 'popup' + this.dataset.uid @@ -458,10 +456,7 @@ class PopupElement extends HTMLElement { let template: HTMLTemplateElement = document.getElementById('popup-template') as HTMLTemplateElement let body = (template.content.cloneNode(true) as DocumentFragment).firstElementChild!; - if (this.dataset.uid === null) { - throw 'UID is required' - } - let uid = this.dataset.uid! + let uid = this.uid; // console.log(uid); body.getElementsByClassName('populate-with-uid') -- cgit v1.2.3 From ae3142da0cf31696f4ab4ad258c5483b7c5490b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Mon, 8 Nov 2021 19:38:42 +0100 Subject: Major work on event creation. --- static/globals.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'static/globals.ts') diff --git a/static/globals.ts b/static/globals.ts index 6b332772..f19312e0 100644 --- a/static/globals.ts +++ b/static/globals.ts @@ -1,5 +1,7 @@ export { - find_block, find_popup, PopupElement + vcal_objects, + find_block, find_popup, PopupElement, + ComponentBlock } import { close_popup, toggle_popup } from './popup' @@ -322,7 +324,7 @@ class DateTimeInput extends /* HTMLInputElement */ HTMLElement { return ['dateonly'] } - attributeChangedCallback(name: string, from: any, to: any) { + attributeChangedCallback(name: string, _: any, to: any): void { // console.log(this, name, boolean(from), boolean(to)); switch (name) { case 'dateonly': -- cgit v1.2.3 From 3d3f46d12abb7d18b33f3b22e51b1a4c154bf85f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Tue, 9 Nov 2021 15:13:33 +0100 Subject: Better handle debug tab. --- static/globals.ts | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) (limited to 'static/globals.ts') diff --git a/static/globals.ts b/static/globals.ts index f19312e0..c812958b 100644 --- a/static/globals.ts +++ b/static/globals.ts @@ -112,7 +112,19 @@ function popuplateTab(tab: HTMLElement, tabgroup: string, index: number) { } } -/* */ +/* */ +class VEventDL extends ComponentVEvent { + redraw(obj: VEvent) { + let dl = buildDescriptionList( + Array.from(obj.boundProperties) + .map(key => [key, obj.getProperty(key)])) + this.replaceChildren(dl); + } +} + +/* + Edit form for a given VEvent. Used as the edit tab of popups. +*/ class ComponentEdit extends ComponentVEvent { firstTime: boolean @@ -206,7 +218,7 @@ function find_popup(uid: uid): HTMLElement | null { // } // } // throw 'Popup not fonud'; - return document.querySelector(`popup-element[data-uid="${uid}"]`) as HTMLElement + return document.querySelector(`popup-element[data-uid="${uid}"]`) } function find_block(uid: uid): HTMLElement | null { @@ -306,6 +318,7 @@ window.addEventListener('load', function() { customElements.define('vevent-description', ComponentDescription); customElements.define('vevent-edit', ComponentEdit); + customElements.define('vevent-dl', VEventDL); customElements.define('vevent-block', ComponentBlock); }) @@ -314,8 +327,11 @@ window.addEventListener('load', function() { /* '' */ class DateTimeInput extends /* HTMLInputElement */ HTMLElement { - constructor() { - super(); + connectedCallback() { + /* This can be in the constructor for chromium, but NOT firefox... + Vivaldi 4.3.2439.63 stable + Mozilla Firefox 94.0.1 + */ this.innerHTML = '' // console.log('constructing datetime input') } @@ -503,23 +519,6 @@ class PopupElement extends ComponentVEvent { // this.value; // event.properties.calendar = this.value; }); - - - - let tab = makeElement('tab-element', { title: 'Debug' }) as TabElement - /// let tab = new TabElement(); - tab.setAttribute('title', 'Debug') - tab.appendChild(makeElement('span', { slot: 'label', innerText: "🐸" })) - // let dl = makeElement('dl', { slot: 'content' }) - let obj = vcal_objects.get(uid)! - let dl = buildDescriptionList( - Array.from(obj.boundProperties) - .map(key => [key, obj.getProperty(key)])) - dl.slot = 'content' - tab.appendChild(dl) - - this.addTab(tab); - // window.setTimeout(() => { this.addTab(tab) }) } addTab(tab: TabElement) { -- cgit v1.2.3 From ca99de5d4a913a5dada84c22b8b3eaf7d3740e8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Wed, 10 Nov 2021 00:15:28 +0100 Subject: Handle calendar change through dropdown. --- static/globals.ts | 71 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 57 insertions(+), 14 deletions(-) (limited to 'static/globals.ts') diff --git a/static/globals.ts b/static/globals.ts index c812958b..daf5a2f7 100644 --- a/static/globals.ts +++ b/static/globals.ts @@ -9,7 +9,8 @@ import { VEvent, xml_to_vcal } from './vevent' import { bind_popup_control } from './dragable' import { uid, parseDate, gensym, to_local, boolean, makeElement } from './lib' -const vcal_objects: Map = new Map() +const vcal_objects: Map = new Map; +(window as any).vcal_objects = vcal_objects; interface HasValue { value: string @@ -146,10 +147,31 @@ class ComponentEdit extends ComponentVEvent { throw `Data missing for uid ${this.dataset.uid}.` } - this.redraw(data); // return; + /* Handle calendar dropdown */ + for (let el of this.getElementsByClassName('calendar-selection')) { + for (let opt of el.getElementsByTagName('option')) { + opt.selected = false; + if (opt.value == event_calendar_mapping.get(this.uid)) { + data.setCalendar(opt.value); + opt.selected = true; + /* No break since we want to set the remainders 'selected' to false */ + } + } + + el.addEventListener('change', (e) => { + let v = (e.target as HTMLSelectElement).selectedOptions[0].value + // e.selectedOptions[0].innerText + + let obj = vcal_objects.get(this.uid)! + obj.setCalendar(v); + }); + } + + this.redraw(data); + for (let el of this.getElementsByClassName("interactive")) { // console.log(el); el.addEventListener('input', () => { @@ -203,6 +225,15 @@ class ComponentEdit extends ComponentVEvent { } } + for (let el of body.getElementsByTagName('calendar-selection')) { + for (let opt of el.getElementsByTagName('option')) { + opt.selected = false; + if (opt.value == data._calendar) { + opt.selected = true; + } + } + } + if (this.firstTime) { this.replaceChildren(body); this.firstTime = false; @@ -284,9 +315,15 @@ class ComponentBlock extends ComponentVEvent { this.style.bottom = result; } } + + if (data._calendar) { + this.dataset.calendar = data._calendar; + } } } +const event_calendar_mapping: Map = new Map; + window.addEventListener('load', function() { // let json_objects_el = document.getElementById('json-objects'); @@ -298,6 +335,15 @@ window.addEventListener('load', function() { vcal_objects.set(ev.getProperty('uid'), ev) } + + let div2 = document.getElementById('calendar-event-mapping')!; + for (let calendar of div2.children) { + for (let child of calendar.children) { + event_calendar_mapping.set( + child.innerHTML, calendar.getAttribute('key')!); + } + } + /* - .popup - .block @@ -466,8 +512,15 @@ class PopupElement extends ComponentVEvent { this.tabcount = 0 } - redraw() { - console.warn('IMPLEMENT ME'); + redraw(data: VEvent) { + // console.warn('IMPLEMENT ME'); + + if (data._calendar) { + this.dataset.calendar = data._calendar; + } + + /* TODO is there any case where we want to propagate the draw to any of + our tabs? or are all our tabs independent? */ } connectedCallback() { @@ -509,16 +562,6 @@ class PopupElement extends ComponentVEvent { /* end nav bar */ this.replaceChildren(body); - - let that = this; - this.getElementsByClassName("calendar-selection")[0] - .addEventListener('change', function() { - let uid = (that.closest('[data-uid]') as HTMLElement).dataset.uid! - let obj = vcal_objects.get(uid) - // TODO this procedure - // this.value; - // event.properties.calendar = this.value; - }); } addTab(tab: TabElement) { -- cgit v1.2.3 From c60a60422f69e29628b6c946a15be271e90015aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Wed, 10 Nov 2021 00:47:10 +0100 Subject: Basic event modification works again. --- static/globals.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'static/globals.ts') diff --git a/static/globals.ts b/static/globals.ts index daf5a2f7..5187d007 100644 --- a/static/globals.ts +++ b/static/globals.ts @@ -8,6 +8,7 @@ import { close_popup, toggle_popup } from './popup' import { VEvent, xml_to_vcal } from './vevent' import { bind_popup_control } from './dragable' import { uid, parseDate, gensym, to_local, boolean, makeElement } from './lib' +import { create_event } from './server_connect' const vcal_objects: Map = new Map; (window as any).vcal_objects = vcal_objects; @@ -188,6 +189,15 @@ class ComponentEdit extends ComponentVEvent { el.value) }); } + + let submit = this.querySelector('form') as HTMLFormElement + submit.addEventListener('submit', (e) => { + console.log(submit, e); + create_event(vcal_objects.get(this.uid)!); + + e.preventDefault(); + return false; + }); } redraw(data: VEvent) { -- cgit v1.2.3 From 410404cfdd54c083b6609fd52334e02d320145d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Wed, 10 Nov 2021 01:40:22 +0100 Subject: Re-modularize javascript. This moves almost everything out of globals.ts, into sepparate files. Things are still slightly to tightly coupled. But that is worked on. --- static/globals.ts | 571 ++---------------------------------------------------- 1 file changed, 14 insertions(+), 557 deletions(-) (limited to 'static/globals.ts') diff --git a/static/globals.ts b/static/globals.ts index 5187d007..be79dae7 100644 --- a/static/globals.ts +++ b/static/globals.ts @@ -1,266 +1,25 @@ export { - vcal_objects, - find_block, find_popup, PopupElement, - ComponentBlock + find_block, + VIEW, EDIT_MODE, + vcal_objects, event_calendar_mapping } -import { close_popup, toggle_popup } from './popup' -import { VEvent, xml_to_vcal } from './vevent' -import { bind_popup_control } from './dragable' -import { uid, parseDate, gensym, to_local, boolean, makeElement } from './lib' -import { create_event } from './server_connect' +import { VEvent } from './vevent' +import { uid } from './types' const vcal_objects: Map = new Map; -(window as any).vcal_objects = vcal_objects; - -interface HasValue { - value: string -} - -function hasValue(obj: any): obj is HasValue { - return 'value' in obj; -} - -/* Root component for all events which content is closely linked to a -@code{VEvent} object - -Lacks an accompaning tag, and shouldn't be directly instanciated. -*/ -class ComponentVEvent extends HTMLElement { - - template: HTMLTemplateElement - uid: string - - constructor(uid?: string) { - super(); - this.template = document.getElementById(this.tagName) as HTMLTemplateElement; - - let real_uid; - if (this.dataset.uid) uid = this.dataset.uid; - if (uid) real_uid = uid; - - if (!real_uid) { - throw `UID required` - } - - this.uid = real_uid; - - vcal_objects.get(this.uid)?.register(this); - - /* We DON'T have a redraw here in the general case, since the - HTML rendered server-side should be fine enough for us. - Those that need a direct rerendering (such as the edit tabs) - should take care of that some other way */ - } - - connectedCallback() { - let uid, v; - if ((uid = this.dataset.uid)) { - v = vcal_objects.get(uid) - if (v) this.redraw(v); - } - } - - redraw(data: VEvent) { - // update ourselves from template - - if (!this.template) { - throw "Something"; - } - - let body = (this.template.content.cloneNode(true) as DocumentFragment).firstElementChild!; - - for (let el of body.getElementsByClassName("bind")) { - if (!(el instanceof HTMLElement)) continue; - let p = el.dataset.property!; - let d, fmt; - if ((d = data.getProperty(p))) { - if ((fmt = el.dataset.fmt)) { - el.innerHTML = d.format(fmt); - } else { - el.innerHTML = d; - } - } - } - - this.replaceChildren(body); - } - -} - - -/* - -*/ -class ComponentDescription extends ComponentVEvent { - constructor() { - super(); - } -} - -function popuplateTab(tab: HTMLElement, tabgroup: string, index: number) { - // console.log(tab); - let new_id = gensym(); - let input = tab.querySelector('input[type="radio"]') as HTMLInputElement; - let label = tab.querySelector("label")! - tab.style.setProperty('--tab-index', '' + index); - /* TODO this throws a number of errors, but somehow still works...? */ - if (input !== null) { - input.name = tabgroup - input.id = new_id; - } - if (label !== null) { - label.setAttribute('for', new_id); - } -} +const event_calendar_mapping: Map = new Map; -/* */ -class VEventDL extends ComponentVEvent { - redraw(obj: VEvent) { - let dl = buildDescriptionList( - Array.from(obj.boundProperties) - .map(key => [key, obj.getProperty(key)])) - this.replaceChildren(dl); +declare global { + interface Window { + vcal_objects: Map; } } +window.vcal_objects = vcal_objects; -/* - Edit form for a given VEvent. Used as the edit tab of popups. -*/ -class ComponentEdit extends ComponentVEvent { - - firstTime: boolean +declare let VIEW: 'month' | 'week' +declare let EDIT_MODE: boolean - constructor() { - super(); - - this.firstTime = true; - } - - connectedCallback() { - - /* Edit tab is rendered here. It's left blank server-side, since - it only makes sense to have something here if we have javascript */ - - let data = vcal_objects.get(this.uid) - - if (!data) { - throw `Data missing for uid ${this.dataset.uid}.` - } - - - // return; - - /* Handle calendar dropdown */ - for (let el of this.getElementsByClassName('calendar-selection')) { - for (let opt of el.getElementsByTagName('option')) { - opt.selected = false; - if (opt.value == event_calendar_mapping.get(this.uid)) { - data.setCalendar(opt.value); - opt.selected = true; - /* No break since we want to set the remainders 'selected' to false */ - } - } - - el.addEventListener('change', (e) => { - let v = (e.target as HTMLSelectElement).selectedOptions[0].value - // e.selectedOptions[0].innerText - - let obj = vcal_objects.get(this.uid)! - obj.setCalendar(v); - }); - } - - this.redraw(data); - - for (let el of this.getElementsByClassName("interactive")) { - // console.log(el); - el.addEventListener('input', () => { - let obj = vcal_objects.get(this.uid) - if (obj === undefined) { - throw 'No object with uid ' + this.uid - } - if (!(hasValue(el) && el instanceof HTMLElement)) { - console.log(el, 'not an HTMLInputElement'); - return; - } - obj.setProperty( - el.dataset.property!, - el.value) - }); - } - - let submit = this.querySelector('form') as HTMLFormElement - submit.addEventListener('submit', (e) => { - console.log(submit, e); - create_event(vcal_objects.get(this.uid)!); - - e.preventDefault(); - return false; - }); - } - - redraw(data: VEvent) { - // update ourselves from template - - if (!this.template) { - throw "Something"; - } - - let body; - if (this.firstTime) { - body = (this.template.content.cloneNode(true) as DocumentFragment).firstElementChild!; - } else { - body = this; - } - - for (let el of body.getElementsByClassName("interactive")) { - if (!(el instanceof HTMLElement)) continue; - let p = el.dataset.property!; - let d: any; - if ((d = data.getProperty(p))) { - /* - https://stackoverflow.com/questions/57157830/how-can-i-specify-the-sequence-of-running-nested-web-components-constructors - */ - window.setTimeout(() => { - /* NOTE Some specific types might require special formatting - here. But due to my custom components implementing custom - `.value' procedures, we might not need any special cases - here */ - /* Technically we just want to cast to HTMLElement with - value field here, but multiple types implement it - sepparately, and no common interface exist */ - (el as HTMLInputElement).value = d; - }); - } - } - - for (let el of body.getElementsByTagName('calendar-selection')) { - for (let opt of el.getElementsByTagName('option')) { - opt.selected = false; - if (opt.value == data._calendar) { - opt.selected = true; - } - } - } - - if (this.firstTime) { - this.replaceChildren(body); - this.firstTime = false; - } - } - -} - -function find_popup(uid: uid): HTMLElement | null { - // for (let el of vcal_objects[uid].registered) { - // if (el.tagName === 'popup-element') { - // return el; - // } - // } - // throw 'Popup not fonud'; - return document.querySelector(`popup-element[data-uid="${uid}"]`) -} function find_block(uid: uid): HTMLElement | null { let obj = vcal_objects.get(uid) @@ -276,319 +35,17 @@ function find_block(uid: uid): HTMLElement | null { return null; } -/* - A grahpical block in the week view. -*/ -class ComponentBlock extends ComponentVEvent { - constructor(uid?: string) { - super(uid); - this.addEventListener('click', () => { - let uid = this.uid - let popup = find_popup(uid); - if (popup === null) throw new Error('no popup for uid ' + uid); - toggle_popup(popup); - }); - } - redraw(data: VEvent) { - super.redraw(data); - let p; - if ((p = data.getProperty('dtstart'))) { - let c = this.closest('.event-container') as HTMLElement - let start = parseDate(c.dataset.start!).getTime() - let end = parseDate(c.dataset.end!).getTime(); - // console.log(p); - let pp = to_local(p).getTime() - let result = 100 * (Math.min(end, Math.max(start, pp)) - start) / (end - start) + "%" - if (c.classList.contains('longevents')) { - this.style.left = result - } else { - this.style.top = result - } - // console.log('dtstart', p); - } - if ((p = data.getProperty('dtend'))) { - // console.log('dtend', p); - let c = this.closest('.event-container') as HTMLElement - let start = parseDate(c.dataset.start!).getTime() - let end = parseDate(c.dataset.end!).getTime(); - let pp = to_local(p).getTime() - let result = 100 - (100 * (Math.min(end, Math.max(start, pp)) - start) / (end - start)) + "%" - if (c.classList.contains('longevents')) { - this.style.width = 'unset'; - this.style.right = result; - } else { - this.style.height = 'unset'; - this.style.bottom = result; - } - } - if (data._calendar) { - this.dataset.calendar = data._calendar; - } - } -} - -const event_calendar_mapping: Map = new Map; - -window.addEventListener('load', function() { - - // let json_objects_el = document.getElementById('json-objects'); - let div = document.getElementById('xcal-data')!; - let vevents = div.firstElementChild!.children; - - for (let vevent of vevents) { - let ev = xml_to_vcal(vevent); - vcal_objects.set(ev.getProperty('uid'), ev) - } - - - let div2 = document.getElementById('calendar-event-mapping')!; - for (let calendar of div2.children) { - for (let child of calendar.children) { - event_calendar_mapping.set( - child.innerHTML, calendar.getAttribute('key')!); - } - } - - /* - - .popup - - .block - - .list - */ - /* - let vevent_els = document.getElementsByClassName('vevent') - for (let el of vevent_els) { - try { - vcal_objects[el.dataset.uid].register(el); - } catch { - console.error("Invalid something, uid = ", el.dataset.uid, - "el = ", el - ); - } - } - */ - - customElements.define('vevent-description', ComponentDescription); - customElements.define('vevent-edit', ComponentEdit); - customElements.define('vevent-dl', VEventDL); - customElements.define('vevent-block', ComponentBlock); -}) - - - - -/* '' */ -class DateTimeInput extends /* HTMLInputElement */ HTMLElement { - connectedCallback() { - /* This can be in the constructor for chromium, but NOT firefox... - Vivaldi 4.3.2439.63 stable - Mozilla Firefox 94.0.1 - */ - this.innerHTML = '' - // console.log('constructing datetime input') - } - - static get observedAttributes() { - return ['dateonly'] - } - - attributeChangedCallback(name: string, _: any, to: any): void { - // console.log(this, name, boolean(from), boolean(to)); - switch (name) { - case 'dateonly': - (this.querySelector('input[type="time"]') as HTMLInputElement) - .disabled = boolean(to) - break; - } - } - - get dateonly(): boolean { - return boolean(this.getAttribute('dateonly')); - } - - set dateonly(bool: boolean) { - this.setAttribute('dateonly', "" + bool); - } - - get valueAsDate(): Date { - let dt; - let date = (this.querySelector("input[type='date']") as HTMLInputElement).value; - if (boolean(this.getAttribute('dateonly'))) { - dt = parseDate(date); - dt.type = 'date'; - } else { - let time = (this.querySelector("input[type='time']") as HTMLInputElement).value; - dt = parseDate(date + 'T' + time) - dt.type = 'date-time'; - } - return dt; - } - - get value(): string { - return this.valueAsDate.format("~Y-~m-~dT~H:~M:~S") - } - - set value(new_value: Date | string) { - // console.log('Setting date'); - let date, time; - if (new_value instanceof Date) { - date = new_value.format("~L~Y-~m-~d"); - time = new_value.format("~L~H:~M:~S"); - } else { - [date, time] = new_value.split('T') - } - (this.querySelector("input[type='date']") as HTMLInputElement).value = date; - (this.querySelector("input[type='time']") as HTMLInputElement).value = time; - } - - addEventListener(type: string, proc: ((e: Event) => void)) { - if (type != 'input') throw "Only input supported"; - - (this.querySelector("input[type='date']") as HTMLInputElement) - .addEventListener(type, proc); - (this.querySelector("input[type='time']") as HTMLInputElement) - .addEventListener(type, proc); - } -} - -customElements.define('date-time-input', DateTimeInput /*, { extends: 'input' } */) - - -function verifySlot(el: Node | null): el is HTMLElement { - if (el === null) { - console.error("Element is null"); - return false; - } - if (!(el instanceof HTMLElement)) { - console.error("Node is not an HTMLElement", el); - return false; - } - return true -} - - -/* */ -class TabElement extends HTMLElement { - constructor() { - super(); - } - - connectedCallback() { - // this.replaceChildren(template.cloneNode(true)); - let template - = (document.getElementById('tab-template') as HTMLTemplateElement) - .content - // const shadowRoot = this.attachShadow({ mode: 'open' }) - // .appendChild(template.cloneNode(true)); - // console.log(this); - let label = this.querySelector('[slot="label"]') - let content = this.querySelector('[slot="content"]') - if (!verifySlot(label)) throw "Bad label"; - if (!verifySlot(content)) throw "Bad content"; - /* TODO set label hover title somewhere around here */ - - this.replaceChildren(template.cloneNode(true)); - this.querySelector('slot[name="label"]')!.replaceWith(label); - this.querySelector('slot[name="content"]')!.replaceWith(content); - } -} - -function buildDescriptionList(data: [string, any][]): HTMLElement { - let dl = document.createElement('dl'); - for (let [key, val] of data) { - dl.appendChild(makeElement('dt', { innerText: key })) - dl.appendChild(makeElement('dd', { innerText: val })) - } - return dl; -} - -/* */ -class PopupElement extends ComponentVEvent { - - tabgroup_id: string - tabcount: number - - constructor(uid?: string) { - super(uid); - - /* TODO populate remaining */ - // this.id = 'popup' + this.dataset.uid - this.tabgroup_id = gensym(); - this.tabcount = 0 - } - - redraw(data: VEvent) { - // console.warn('IMPLEMENT ME'); - - if (data._calendar) { - this.dataset.calendar = data._calendar; - } - - /* TODO is there any case where we want to propagate the draw to any of - our tabs? or are all our tabs independent? */ - } - - connectedCallback() { - let template: HTMLTemplateElement = document.getElementById('popup-template') as HTMLTemplateElement - let body = (template.content.cloneNode(true) as DocumentFragment).firstElementChild!; - - let uid = this.uid; - // console.log(uid); - - body.getElementsByClassName('populate-with-uid') - .forEach((e) => e.setAttribute('data-uid', uid)); - - /* tabs */ - // for (let tab of body.querySelectorAll(".tabgroup .tab")) { - // } - window.setTimeout(() => { - // let tabs = this.querySelector('tab-element')! - // .shadowRoot! - // .querySelectorAll('label') - // console.log(tabs); - // console.log(this.getElementsByTagName('tab-element')) - for (let tab of this.getElementsByTagName('tab-element')) { - // console.log(tab_container); - // let tab = tab_container.shadowRoot!; - // tab.documentElement.style.setProperty('--i', i); - popuplateTab(tab as TabElement, this.tabgroup_id, this.tabcount) - this.tabcount += 1 - } - (this.querySelector('tab-element label') as HTMLInputElement).click() - }); - /* end tabs */ - - /* nav bar */ - let nav = body.getElementsByClassName("popup-control")[0] as HTMLElement; - bind_popup_control(nav); - - let btn = body.querySelector('.popup-control .close-tooltip') as HTMLButtonElement - btn.addEventListener('click', () => close_popup(this)); - /* end nav bar */ - - this.replaceChildren(body); - } - - addTab(tab: TabElement) { - let tabgroup = this.getElementsByClassName('tabgroup')![0]! - tabgroup.append(tab); - popuplateTab(tab, this.tabgroup_id, this.tabcount) - this.tabcount += 1 - } -} - -window.addEventListener('load', function() { - customElements.define('popup-element', PopupElement) - customElements.define('tab-element', TabElement) -}); +/* function wholeday_checkbox(box: HTMLInputElement) { box.closest('.timeinput')! .querySelectorAll('input[is="date-time"]') .forEach((el) => { (el as DateTimeInput).dateonly = box.checked }); } +*/ -- cgit v1.2.3 From 93c40e8eeaef8e1609759fb1f0cad1fc9285ab86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Thu, 11 Nov 2021 16:28:49 +0100 Subject: Made VIEW and EDIT_MODE correctly global. --- static/globals.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) (limited to 'static/globals.ts') diff --git a/static/globals.ts b/static/globals.ts index be79dae7..b0871aa2 100644 --- a/static/globals.ts +++ b/static/globals.ts @@ -1,6 +1,5 @@ export { find_block, - VIEW, EDIT_MODE, vcal_objects, event_calendar_mapping } @@ -13,14 +12,12 @@ const event_calendar_mapping: Map = new Map; declare global { interface Window { vcal_objects: Map; + VIEW: 'month' | 'week'; + EDIT_MODE: boolean; } } window.vcal_objects = vcal_objects; -declare let VIEW: 'month' | 'week' -declare let EDIT_MODE: boolean - - function find_block(uid: uid): HTMLElement | null { let obj = vcal_objects.get(uid) if (obj === undefined) { -- cgit v1.2.3 From 7b604002e5dd73f933ef89b37e60ab07d347c44d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Mon, 15 Nov 2021 00:43:49 +0100 Subject: Reword popup visible attribute. Now all logic for handling hiding and showing popups are inside the PopupElement class, making it much harder to do stuff incorrectly. It also slowly releases the knot around popup.ts. --- static/globals.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'static/globals.ts') diff --git a/static/globals.ts b/static/globals.ts index b0871aa2..7ccfb3d2 100644 --- a/static/globals.ts +++ b/static/globals.ts @@ -5,6 +5,7 @@ export { import { VEvent } from './vevent' import { uid } from './types' +import { ComponentBlock } from './components/vevent-block' const vcal_objects: Map = new Map; const event_calendar_mapping: Map = new Map; @@ -18,14 +19,14 @@ declare global { } window.vcal_objects = vcal_objects; -function find_block(uid: uid): HTMLElement | null { +function find_block(uid: uid): ComponentBlock | null { let obj = vcal_objects.get(uid) if (obj === undefined) { return null; } for (let el of obj.registered) { if (el.tagName === 'vevent-block') { - return el; + return el as ComponentBlock; } } // throw 'Popup not fonud'; -- cgit v1.2.3 From e8216ceb2cb5c61ef6fd6d6f6c9151511d01d8e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Mon, 15 Nov 2021 01:39:46 +0100 Subject: Propagate default-calendar from backend to frontend. --- static/globals.ts | 1 + 1 file changed, 1 insertion(+) (limited to 'static/globals.ts') diff --git a/static/globals.ts b/static/globals.ts index 7ccfb3d2..6b689697 100644 --- a/static/globals.ts +++ b/static/globals.ts @@ -15,6 +15,7 @@ declare global { vcal_objects: Map; VIEW: 'month' | 'week'; EDIT_MODE: boolean; + default_calendar: string; } } window.vcal_objects = vcal_objects; -- cgit v1.2.3 From 8346e5b938c2a01128e8806dbe2b57e4266c51a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Thu, 18 Nov 2021 21:39:49 +0100 Subject: Remove .interactive, fix date-time checkbox. --- static/globals.ts | 15 --------------- 1 file changed, 15 deletions(-) (limited to 'static/globals.ts') diff --git a/static/globals.ts b/static/globals.ts index 6b689697..cb65d953 100644 --- a/static/globals.ts +++ b/static/globals.ts @@ -33,18 +33,3 @@ function find_block(uid: uid): ComponentBlock | null { // throw 'Popup not fonud'; return null; } - - - - - - - - -/* -function wholeday_checkbox(box: HTMLInputElement) { - box.closest('.timeinput')! - .querySelectorAll('input[is="date-time"]') - .forEach((el) => { (el as DateTimeInput).dateonly = box.checked }); -} -*/ -- cgit v1.2.3 From 12d9658e95ec981239a8391e75af784efe78f47f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Fri, 10 Dec 2021 16:18:47 +0100 Subject: Add create event button! --- static/globals.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) (limited to 'static/globals.ts') diff --git a/static/globals.ts b/static/globals.ts index cb65d953..eb7488c0 100644 --- a/static/globals.ts +++ b/static/globals.ts @@ -7,6 +7,9 @@ import { VEvent } from './vevent' import { uid } from './types' import { ComponentBlock } from './components/vevent-block' +import { v4 as uuid } from 'uuid' +import { setup_popup_element } from './components/popup-element' + const vcal_objects: Map = new Map; const event_calendar_mapping: Map = new Map; @@ -16,10 +19,30 @@ declare global { VIEW: 'month' | 'week'; EDIT_MODE: boolean; default_calendar: string; + + addNewEvent: ((e: any) => void); } } window.vcal_objects = vcal_objects; + +window.addNewEvent = () => { + let ev = new VEvent(); + let uid = uuid() + let now = new Date() + ev.setProperties([ + ['uid', uid], + ['dtstart', now, 'date-time'], + ['dtend', new Date(now.getTime() + 3600 * 1000), 'date-time'], + ]) + ev.calendar = window.default_calendar; + + vcal_objects.set(uid, ev); + + let popup = setup_popup_element(ev); + popup.maximize(); +} + function find_block(uid: uid): ComponentBlock | null { let obj = vcal_objects.get(uid) if (obj === undefined) { -- cgit v1.2.3