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/components/date-time-input.ts | 77 ++++++++++++++++++ static/components/popup-element.ts | 100 +++++++++++++++++++++++ static/components/tab-element.ts | 40 ++++++++++ static/components/vevent-block.ts | 63 +++++++++++++++ static/components/vevent-description.ts | 12 +++ static/components/vevent-dl.ts | 24 ++++++ static/components/vevent-edit.ts | 137 ++++++++++++++++++++++++++++++++ static/components/vevent.ts | 71 +++++++++++++++++ 8 files changed, 524 insertions(+) create mode 100644 static/components/date-time-input.ts create mode 100644 static/components/popup-element.ts create mode 100644 static/components/tab-element.ts create mode 100644 static/components/vevent-block.ts create mode 100644 static/components/vevent-description.ts create mode 100644 static/components/vevent-dl.ts create mode 100644 static/components/vevent-edit.ts create mode 100644 static/components/vevent.ts (limited to 'static/components') diff --git a/static/components/date-time-input.ts b/static/components/date-time-input.ts new file mode 100644 index 00000000..1f54b15e --- /dev/null +++ b/static/components/date-time-input.ts @@ -0,0 +1,77 @@ +export { DateTimeInput } + +import { to_boolean, parseDate } from '../lib' + +/* '' */ +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, to_boolean(from), to_boolean(to)); + switch (name) { + case 'dateonly': + (this.querySelector('input[type="time"]') as HTMLInputElement) + .disabled = to_boolean(to) + break; + } + } + + get dateonly(): boolean { + return to_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 (to_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); + } +} diff --git a/static/components/popup-element.ts b/static/components/popup-element.ts new file mode 100644 index 00000000..3225fa52 --- /dev/null +++ b/static/components/popup-element.ts @@ -0,0 +1,100 @@ +export { PopupElement } + +import { gensym } from '../lib' +import { VEvent } from '../vevent' +import { bind_popup_control } from '../dragable' +import { close_popup } from '../popup' + +import { ComponentVEvent } from './vevent' +import { TabElement } from './tab-element' + +/* */ +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 + } +} + +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); + } +} diff --git a/static/components/tab-element.ts b/static/components/tab-element.ts new file mode 100644 index 00000000..9403a737 --- /dev/null +++ b/static/components/tab-element.ts @@ -0,0 +1,40 @@ +export { TabElement } + +/* */ +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 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 +} diff --git a/static/components/vevent-block.ts b/static/components/vevent-block.ts new file mode 100644 index 00000000..439ba20e --- /dev/null +++ b/static/components/vevent-block.ts @@ -0,0 +1,63 @@ +export { ComponentBlock } + +import { ComponentVEvent } from './vevent' +import { VEvent } from '../vevent' +import { toggle_popup, find_popup } from '../popup' +import { parseDate, to_local } from '../lib' + + +/* + + 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; + } + } +} diff --git a/static/components/vevent-description.ts b/static/components/vevent-description.ts new file mode 100644 index 00000000..f97b60e1 --- /dev/null +++ b/static/components/vevent-description.ts @@ -0,0 +1,12 @@ +export { ComponentDescription } + +import { ComponentVEvent } from './vevent' + +/* + +*/ +class ComponentDescription extends ComponentVEvent { + constructor() { + super(); + } +} diff --git a/static/components/vevent-dl.ts b/static/components/vevent-dl.ts new file mode 100644 index 00000000..a9e60d81 --- /dev/null +++ b/static/components/vevent-dl.ts @@ -0,0 +1,24 @@ +export { VEventDL } + +import { ComponentVEvent } from './vevent' +import { VEvent } from '../vevent' +import { makeElement } from '../lib' + +/* */ +class VEventDL extends ComponentVEvent { + redraw(obj: VEvent) { + let dl = buildDescriptionList( + Array.from(obj.boundProperties) + .map(key => [key, obj.getProperty(key)])) + this.replaceChildren(dl); + } +} + +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; +} diff --git a/static/components/vevent-edit.ts b/static/components/vevent-edit.ts new file mode 100644 index 00000000..602e1872 --- /dev/null +++ b/static/components/vevent-edit.ts @@ -0,0 +1,137 @@ +export { ComponentEdit } + +import { ComponentVEvent } from './vevent' +import { DateTimeInput } from './date-time-input' + +import { vcal_objects, event_calendar_mapping } from '../globals' +import { VEvent } from '../vevent' +import { create_event } from '../server_connect' + +/* + Edit form for a given VEvent. Used as the edit tab of popups. +*/ +class ComponentEdit extends ComponentVEvent { + + firstTime: 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 (!(el instanceof HTMLInputElement + || el instanceof DateTimeInput)) { + 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; + } + } + +} diff --git a/static/components/vevent.ts b/static/components/vevent.ts new file mode 100644 index 00000000..de232794 --- /dev/null +++ b/static/components/vevent.ts @@ -0,0 +1,71 @@ +export { ComponentVEvent } + +import { vcal_objects } from '../globals' +import { VEvent } from '../vevent' + +/* 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); + } + +} -- 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/components/popup-element.ts | 54 ++++++++++++++++++++++++++++++++++---- static/components/vevent-block.ts | 4 +-- 2 files changed, 51 insertions(+), 7 deletions(-) (limited to 'static/components') diff --git a/static/components/popup-element.ts b/static/components/popup-element.ts index 3225fa52..bad7476a 100644 --- a/static/components/popup-element.ts +++ b/static/components/popup-element.ts @@ -3,7 +3,7 @@ export { PopupElement } import { gensym } from '../lib' import { VEvent } from '../vevent' import { bind_popup_control } from '../dragable' -import { close_popup } from '../popup' +import { close_popup, event_from_popup } from '../popup' import { ComponentVEvent } from './vevent' import { TabElement } from './tab-element' @@ -14,11 +14,13 @@ class PopupElement extends ComponentVEvent { tabgroup_id: string tabcount: number + isVisible: boolean = false; + constructor(uid?: string) { super(uid); - /* TODO populate remaining */ - // this.id = 'popup' + this.dataset.uid + /* TODO populate remaining (??) */ + this.tabgroup_id = gensym(); this.tabcount = 0 } @@ -26,8 +28,9 @@ class PopupElement extends ComponentVEvent { redraw(data: VEvent) { // console.warn('IMPLEMENT ME'); - if (data._calendar) { - this.dataset.calendar = data._calendar; + console.log('popup', data.calendar); + if (data.calendar) { + this.dataset.calendar = data.calendar; } /* TODO is there any case where we want to propagate the draw to any of @@ -75,6 +78,47 @@ class PopupElement extends ComponentVEvent { this.replaceChildren(body); } + static get observedAttributes() { + return ['visible']; + } + + get visible(): boolean { + return this.isVisible; + } + + set visible(isVisible: boolean) { + this.isVisible = isVisible; + if (this.isVisible) { + this.classList.add('visible'); + } else { + this.classList.remove('visible'); + } + + let root; + switch (window.VIEW) { + case 'week': + root = document.getElementsByClassName("days")[0]; + break; + case 'month': + default: + root = document.body; + break; + } + + let element = event_from_popup(this) as HTMLElement; + /* start sets offset between top left corner + of event in calendar and popup. 10, 10 soo old + event is still visible */ + let offsetX = 10, offsetY = 10; + while (element !== root && element !== null) { + offsetX += element.offsetLeft; + offsetY += element.offsetTop; + element = element.offsetParent as HTMLElement; + } + this.style.left = offsetX + "px"; + this.style.top = offsetY + "px"; + } + addTab(tab: TabElement) { let tabgroup = this.getElementsByClassName('tabgroup')![0]! tabgroup.append(tab); diff --git a/static/components/vevent-block.ts b/static/components/vevent-block.ts index 439ba20e..8c6d11cf 100644 --- a/static/components/vevent-block.ts +++ b/static/components/vevent-block.ts @@ -56,8 +56,8 @@ class ComponentBlock extends ComponentVEvent { } } - if (data._calendar) { - this.dataset.calendar = data._calendar; + if (data.calendar) { + this.dataset.calendar = data.calendar; } } } -- cgit v1.2.3 From ecdb4b7eeb42dc859cfd6aa31634b423b72c50bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Mon, 15 Nov 2021 00:47:13 +0100 Subject: Rework some drawing and how calendar is accessed. --- static/components/vevent-edit.ts | 49 +++++++++++----------------------------- static/components/vevent.ts | 7 +++--- 2 files changed, 17 insertions(+), 39 deletions(-) (limited to 'static/components') diff --git a/static/components/vevent-edit.ts b/static/components/vevent-edit.ts index 602e1872..b9c76dd3 100644 --- a/static/components/vevent-edit.ts +++ b/static/components/vevent-edit.ts @@ -12,12 +12,12 @@ import { create_event } from '../server_connect' */ class ComponentEdit extends ComponentVEvent { - firstTime: boolean - constructor() { super(); - this.firstTime = true; + let frag = this.template.content.cloneNode(true) as DocumentFragment + let body = frag.firstElementChild! + this.replaceChildren(body); } connectedCallback() { @@ -38,19 +38,15 @@ class ComponentEdit extends ComponentVEvent { 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 */ - } + } + if (data.calendar) { + (el as HTMLSelectElement).value = data.calendar; } 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); + obj.calendar = v; }); } @@ -85,20 +81,10 @@ class ComponentEdit extends ComponentVEvent { } 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; - } + /* We only update our fields, instead of reinstansiating + ourselves from the template, in hope that it's faster */ - for (let el of body.getElementsByClassName("interactive")) { + for (let el of this.getElementsByClassName("interactive")) { if (!(el instanceof HTMLElement)) continue; let p = el.dataset.property!; let d: any; @@ -119,19 +105,10 @@ 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 (data.calendar) { + for (let el of this.getElementsByClassName('calendar-selection')) { + (el as HTMLSelectElement).value = data.calendar; } } - - if (this.firstTime) { - this.replaceChildren(body); - this.firstTime = false; - } } - } diff --git a/static/components/vevent.ts b/static/components/vevent.ts index de232794..d957c7c8 100644 --- a/static/components/vevent.ts +++ b/static/components/vevent.ts @@ -26,6 +26,7 @@ class ComponentVEvent extends HTMLElement { } this.uid = real_uid; + this.dataset.uid = uid; vcal_objects.get(this.uid)?.register(this); @@ -36,9 +37,9 @@ class ComponentVEvent extends HTMLElement { } connectedCallback() { - let uid, v; - if ((uid = this.dataset.uid)) { - v = vcal_objects.get(uid) + let uid = this.dataset.uid + if (uid) { + let v = vcal_objects.get(uid) if (v) this.redraw(v); } } -- cgit v1.2.3 From ec9f95e5929ad7fd86909002b851e3fa71d2a46b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Mon, 15 Nov 2021 01:32:29 +0100 Subject: Fix calendar for popup. --- static/components/popup-element.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'static/components') diff --git a/static/components/popup-element.ts b/static/components/popup-element.ts index bad7476a..2f019f4d 100644 --- a/static/components/popup-element.ts +++ b/static/components/popup-element.ts @@ -4,6 +4,7 @@ import { gensym } from '../lib' import { VEvent } from '../vevent' import { bind_popup_control } from '../dragable' import { close_popup, event_from_popup } from '../popup' +import { vcal_objects } from '../globals' import { ComponentVEvent } from './vevent' import { TabElement } from './tab-element' @@ -23,12 +24,14 @@ class PopupElement extends ComponentVEvent { this.tabgroup_id = gensym(); this.tabcount = 0 + + let obj = vcal_objects.get(this.uid); + if (obj && obj.calendar) { + this.dataset.calendar = obj.calendar; + } } redraw(data: VEvent) { - // console.warn('IMPLEMENT ME'); - - console.log('popup', data.calendar); if (data.calendar) { this.dataset.calendar = data.calendar; } -- cgit v1.2.3 From 3396442d94214ea811d29090a50c05dffa97ad0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Thu, 18 Nov 2021 21:38:46 +0100 Subject: Various fixes --- static/components/date-time-input.ts | 56 +++++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 13 deletions(-) (limited to 'static/components') diff --git a/static/components/date-time-input.ts b/static/components/date-time-input.ts index 1f54b15e..a9d10ba3 100644 --- a/static/components/date-time-input.ts +++ b/static/components/date-time-input.ts @@ -1,44 +1,68 @@ export { DateTimeInput } -import { to_boolean, parseDate } from '../lib' +import { makeElement, parseDate } from '../lib' + /* '' */ 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') + /* + https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes#boolean_attributes + https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute + */ + this.replaceChildren( + makeElement('input', { type: 'date' }), + makeElement('input', { + type: 'time', + disabled: this.hasAttribute('dateonly') + }) + ) } static get observedAttributes() { return ['dateonly'] } - attributeChangedCallback(name: string, _: any, to: any): void { - // console.log(this, name, to_boolean(from), to_boolean(to)); + attributeChangedCallback(name: string, _: string | null, to: string | null): void { switch (name) { case 'dateonly': - (this.querySelector('input[type="time"]') as HTMLInputElement) - .disabled = to_boolean(to) + let time = this.querySelector('input[type="time"]') as HTMLInputElement | null + /* should only be possible on creation whith dateonly="" sat. */ + if (!time) return; + if (to == null) { + time.disabled = false + } else { + if (to == '' || to == name) { + time.disabled = true; + } else { + throw new TypeError(`Invalid value for attribute dateonly: ${to}`) + } + } break; } } get dateonly(): boolean { - return to_boolean(this.getAttribute('dateonly')); + return this.hasAttribute('dateonly'); } - set dateonly(bool: boolean) { - this.setAttribute('dateonly', "" + bool); + set dateonly(b: boolean) { + if (b) { + this.setAttribute('dateonly', ""); + } else { + this.removeAttribute('dateonly'); + } } get valueAsDate(): Date { let dt; let date = (this.querySelector("input[type='date']") as HTMLInputElement).value; - if (to_boolean(this.getAttribute('dateonly'))) { + if (this.hasAttribute('dateonly')) { dt = parseDate(date); dt.type = 'date'; } else { @@ -50,18 +74,24 @@ class DateTimeInput extends /* HTMLInputElement */ HTMLElement { } get value(): string { - return this.valueAsDate.format("~Y-~m-~dT~H:~M:~S") + if (this.dateonly) { + return this.valueAsDate.format("~Y-~m-~d") + } else { + return this.valueAsDate.format("~Y-~m-~dT~H:~M:~S") + } } set value(new_value: Date | string) { // console.log('Setting date'); - let date, time; + let date, time, dateonly = false; if (new_value instanceof Date) { date = new_value.format("~L~Y-~m-~d"); time = new_value.format("~L~H:~M:~S"); + dateonly = new_value.dateonly; } else { [date, time] = new_value.split('T') } + this.dateonly = dateonly; (this.querySelector("input[type='date']") as HTMLInputElement).value = date; (this.querySelector("input[type='time']") as HTMLInputElement).value = time; } -- 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/components/vevent-edit.ts | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) (limited to 'static/components') diff --git a/static/components/vevent-edit.ts b/static/components/vevent-edit.ts index b9c76dd3..e393b1ce 100644 --- a/static/components/vevent-edit.ts +++ b/static/components/vevent-edit.ts @@ -52,7 +52,8 @@ class ComponentEdit extends ComponentVEvent { this.redraw(data); - for (let el of this.getElementsByClassName("interactive")) { + // for (let el of this.getElementsByClassName("interactive")) { + for (let el of this.querySelectorAll("[data-property]")) { // console.log(el); el.addEventListener('input', () => { let obj = vcal_objects.get(this.uid) @@ -70,6 +71,25 @@ class ComponentEdit extends ComponentVEvent { }); } + let wholeday_ = this.querySelector('[name="wholeday"]') + if (wholeday_) { + let wholeday = wholeday_ as HTMLInputElement + + if (data.getProperty('dtstart')?.dateonly) { + wholeday.checked = true; + } + + wholeday.addEventListener('click', () => { + let chk = wholeday.checked + let start = data!.getProperty('dtstart') + let end = data!.getProperty('dtend') + start.dateonly = chk + end.dateonly = chk + data!.setProperty('dtstart', start); + data!.setProperty('dtend', end); + }); + } + let submit = this.querySelector('form') as HTMLFormElement submit.addEventListener('submit', (e) => { console.log(submit, e); @@ -84,7 +104,8 @@ class ComponentEdit extends ComponentVEvent { /* We only update our fields, instead of reinstansiating ourselves from the template, in hope that it's faster */ - for (let el of this.getElementsByClassName("interactive")) { + + for (let el of this.querySelectorAll("[data-property]")) { if (!(el instanceof HTMLElement)) continue; let p = el.dataset.property!; let d: any; -- cgit v1.2.3 From 5cd004b662caabc4a084d625da798b1b64d8b5c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Thu, 18 Nov 2021 21:41:01 +0100 Subject: Minor fixes. --- static/components/vevent-dl.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'static/components') diff --git a/static/components/vevent-dl.ts b/static/components/vevent-dl.ts index a9e60d81..75de075b 100644 --- a/static/components/vevent-dl.ts +++ b/static/components/vevent-dl.ts @@ -18,7 +18,14 @@ 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 })) + let fmtVal: string = val; + if (val instanceof Date) { + fmtVal = val.format( + val.dateonly + ? '~Y-~m-~d' + : '~Y-~m-~dT~H:~M:~S'); + } + dl.appendChild(makeElement('dd', { innerText: fmtVal })) } return dl; } -- cgit v1.2.3 From e5219a712560fde7c843652f088de995a6de68ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Thu, 18 Nov 2021 21:41:20 +0100 Subject: Restore remove event functionality. --- static/components/popup-element.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) (limited to 'static/components') diff --git a/static/components/popup-element.ts b/static/components/popup-element.ts index 2f019f4d..cb67035f 100644 --- a/static/components/popup-element.ts +++ b/static/components/popup-element.ts @@ -9,6 +9,8 @@ import { vcal_objects } from '../globals' import { ComponentVEvent } from './vevent' import { TabElement } from './tab-element' +import { remove_event } from '../server_connect' + /* */ class PopupElement extends ComponentVEvent { @@ -74,8 +76,11 @@ class PopupElement extends ComponentVEvent { 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)); + let close_btn = body.querySelector('.popup-control .close-button') as HTMLButtonElement + close_btn.addEventListener('click', () => close_popup(this)); + + let remove_btn = body.querySelector('.popup-control .remove-button') as HTMLButtonElement + remove_btn.addEventListener('click', () => remove_event(uid)); /* end nav bar */ this.replaceChildren(body); -- cgit v1.2.3 From 74102c3f5f400542a09c5c5fc1043ab5fa490be0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Thu, 18 Nov 2021 23:15:35 +0100 Subject: Change popup tabs interface. --- static/components/tab-element.ts | 28 ++++++++-------------------- static/components/vevent.ts | 1 + 2 files changed, 9 insertions(+), 20 deletions(-) (limited to 'static/components') diff --git a/static/components/tab-element.ts b/static/components/tab-element.ts index 9403a737..9da6c504 100644 --- a/static/components/tab-element.ts +++ b/static/components/tab-element.ts @@ -7,34 +7,22 @@ class TabElement extends HTMLElement { } 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 */ + let content = Array.from(this.children, (e) => e.cloneNode(true)) this.replaceChildren(template.cloneNode(true)); - this.querySelector('slot[name="label"]')!.replaceWith(label); - this.querySelector('slot[name="content"]')!.replaceWith(content); - } -} -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; + let label = this.querySelector('label') + if (!label) throw "Invalid tab" + + label.setAttribute('title', this.getAttribute('label-title') || '') + label.innerText = this.getAttribute('label') || 'T' + + this.querySelector('slot[name="content"]')!.replaceWith(...content); } - return true } diff --git a/static/components/vevent.ts b/static/components/vevent.ts index d957c7c8..23eba0a9 100644 --- a/static/components/vevent.ts +++ b/static/components/vevent.ts @@ -22,6 +22,7 @@ class ComponentVEvent extends HTMLElement { if (uid) real_uid = uid; if (!real_uid) { + console.warn(this.outerHTML); throw `UID required` } -- cgit v1.2.3 From 215bbfb82465a6cddb286feb5168c2a15475bbea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Fri, 19 Nov 2021 01:34:33 +0100 Subject: vevent-dl formats rrules. --- static/components/vevent-dl.ts | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'static/components') diff --git a/static/components/vevent-dl.ts b/static/components/vevent-dl.ts index 75de075b..dfab8183 100644 --- a/static/components/vevent-dl.ts +++ b/static/components/vevent-dl.ts @@ -4,6 +4,8 @@ import { ComponentVEvent } from './vevent' import { VEvent } from '../vevent' import { makeElement } from '../lib' +import { RecurrenceRule } from '../vevent' + /* */ class VEventDL extends ComponentVEvent { redraw(obj: VEvent) { @@ -24,6 +26,8 @@ function buildDescriptionList(data: [string, any][]): HTMLElement { val.dateonly ? '~Y-~m-~d' : '~Y-~m-~dT~H:~M:~S'); + } else if (val instanceof RecurrenceRule) { + fmtVal = JSON.stringify(val.to_jcal()) } dl.appendChild(makeElement('dd', { innerText: fmtVal })) } -- cgit v1.2.3 From e1a05a727f91622bfcf2c7c9025592c51f1abd20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Fri, 19 Nov 2021 16:43:12 +0100 Subject: Add input-list custom element. --- static/components/input-list.ts | 59 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 static/components/input-list.ts (limited to 'static/components') diff --git a/static/components/input-list.ts b/static/components/input-list.ts new file mode 100644 index 00000000..326cb2b5 --- /dev/null +++ b/static/components/input-list.ts @@ -0,0 +1,59 @@ +export { InputList } + +/* This file replaces input_list.js */ + +class InputList extends HTMLElement { + + el: HTMLInputElement; + + values: [HTMLInputElement, any][] = []; + + constructor() { + super(); + this.el = this.children[0].cloneNode(true) as HTMLInputElement; + } + + connectedCallback() { + this.addInstance(); + } + + addInstance() { + let new_el = this.el.cloneNode(true) as HTMLInputElement + let that = this; + new_el.addEventListener('input', function() { + if (new_el.value === '') { + let sibling = this.previousElementSibling || this.nextElementSibling; + // this.remove(); + // that.values = that.values.filter((p) => p[0] == this) + that.values = that.values.filter((p) => p[0] != this); + this.remove(); + (sibling as HTMLInputElement).focus(); + } else { + if (!this.nextElementSibling) { + that.addInstance(); + // window.setTimeout(() => this.focus()) + this.focus(); + } + } + }); + this.values.push([new_el, '']) + // this.appendChild(new_el); + this.replaceChildren(... this.values.map((p) => p[0])) + } + + get value(): any[] { + return [] + } + + set value(new_value: any[]) { + let els = []; + for (let value of new_value) { + let new_el = this.el.cloneNode() as HTMLInputElement; + new_el.value = value; + els.push(new_el); + } + /* Final element (empty) */ + els.push(this.el.cloneNode() as HTMLInputElement); + this.replaceChildren(...els); + } +} -- cgit v1.2.3 From 3bd26b63adf49ec1b3f52897008738f13b864451 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Fri, 19 Nov 2021 16:45:06 +0100 Subject: Add basic rrule tab. --- static/components/edit-rrule.ts | 51 +++++++++++++++++++++++++++++++++++++++ static/components/vevent-block.ts | 4 +++ 2 files changed, 55 insertions(+) create mode 100644 static/components/edit-rrule.ts (limited to 'static/components') diff --git a/static/components/edit-rrule.ts b/static/components/edit-rrule.ts new file mode 100644 index 00000000..a4d09083 --- /dev/null +++ b/static/components/edit-rrule.ts @@ -0,0 +1,51 @@ +export { EditRRule } + +import { ComponentVEvent } from './vevent' +import { VEvent } from '../vevent' +import { vcal_objects } from '../globals' + +import { RecurrenceRule } from '../vevent' + +/* */ +class EditRRule extends ComponentVEvent { + + constructor() { + super(); + + let frag = this.template.content.cloneNode(true) as DocumentFragment + let body = frag.firstElementChild! + this.replaceChildren(body); + } + + connectedCallback() { + this.redraw(vcal_objects.get(this.uid)!) + } + + redraw(data: VEvent) { + + let rrule = data.getProperty('rrule') + if (!rrule) return; + rrule = rrule as RecurrenceRule + + for (let el of this.querySelectorAll('[name]')) { + + /* + el ought to be one of the tag types: + , ,