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