diff options
Diffstat (limited to 'static/components')
-rw-r--r-- | static/components/changelog.ts | 49 | ||||
-rw-r--r-- | static/components/date-jump.ts | 40 | ||||
-rw-r--r-- | static/components/date-time-input.ts | 119 | ||||
-rw-r--r-- | static/components/edit-rrule.ts | 75 | ||||
-rw-r--r-- | static/components/input-list.ts | 120 | ||||
-rw-r--r-- | static/components/popup-element.ts | 201 | ||||
-rw-r--r-- | static/components/slider.ts | 101 | ||||
-rw-r--r-- | static/components/tab-group-element.ts | 184 | ||||
-rw-r--r-- | static/components/vevent-block.ts | 99 | ||||
-rw-r--r-- | static/components/vevent-description.ts | 38 | ||||
-rw-r--r-- | static/components/vevent-dl.ts | 35 | ||||
-rw-r--r-- | static/components/vevent-edit.ts | 179 | ||||
-rw-r--r-- | static/components/vevent.ts | 69 |
13 files changed, 0 insertions, 1309 deletions
diff --git a/static/components/changelog.ts b/static/components/changelog.ts deleted file mode 100644 index d08f7cb3..00000000 --- a/static/components/changelog.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { makeElement } from '../lib' -import { ComponentVEvent } from './vevent' -import { VEvent } from '../vevent' - -export { VEventChangelog } - -class VEventChangelog extends ComponentVEvent { - - readonly ul: HTMLElement - - constructor(uid?: string) { - super(uid); - - this.ul = makeElement('ul'); - } - - connectedCallback() { - this.replaceChildren(this.ul); - } - - redraw(data: VEvent) { - /* TODO only redraw what is needed */ - let children = [] - for (let [_, el] of data.changelog) { - let msg = ''; - switch (el.type) { - case 'property': - msg += `change ${el.name}: ` - msg += `from "${el.from}" to "${el.to}"` - break; - case 'calendar': - if (el.from === null && el.to === null) { - msg += '???' - } else if (el.from === null) { - msg += `set calendar to "${atob(el.to!)}"` - } else if (el.to === null) { - msg += `Remove calendar "${atob(el.from)}"` - } else { - msg += `Change calendar from "${atob(el.from)}" to "${atob(el.to)}"` - } - break; - } - - children.push(makeElement('li', { textContent: msg })); - } - - this.ul.replaceChildren(...children) - } -} diff --git a/static/components/date-jump.ts b/static/components/date-jump.ts deleted file mode 100644 index fd3908ae..00000000 --- a/static/components/date-jump.ts +++ /dev/null @@ -1,40 +0,0 @@ -export { DateJump } - -/* Replace backend-driven [today] link with frontend, with one that - gets correctly set in the frontend. Similarly, update the go to - specific date button into a link which updates wheneven the date - form updates. -*/ -class DateJump extends HTMLElement { - - readonly golink: HTMLAnchorElement; - readonly input: HTMLInputElement; - - constructor() { - super(); - - this.golink = document.createElement('a') - this.golink.classList.add('btn'); - this.golink.textContent = "➔" - this.input = document.createElement('input') - this.input.type = 'date'; - } - - connectedCallback() { - - /* Form is just here so the css works out */ - let form = document.createElement('form'); - form.replaceChildren(this.input, this.golink); - this.replaceChildren(form); - - this.input.onchange = () => { - let date = this.input.valueAsDate!.format('~Y-~m-~d'); - this.golink.href = `${date}.html` - } - - let now = (new Date).format("~Y-~m-~d") - this.input.value = now; - /* onchange isn't triggered by manually setting the value */ - this.golink.href = `${now}.html` - } -} diff --git a/static/components/date-time-input.ts b/static/components/date-time-input.ts deleted file mode 100644 index 20e9a505..00000000 --- a/static/components/date-time-input.ts +++ /dev/null @@ -1,119 +0,0 @@ -export { DateTimeInput } - -import { makeElement, parseDate } from '../lib' - - -/* '<date-time-input />' */ -class DateTimeInput extends /* HTMLInputElement */ HTMLElement { - - readonly time: HTMLInputElement; - readonly date: HTMLInputElement; - - constructor() { - super(); - - this.date = makeElement('input', { - type: 'date' - }) as HTMLInputElement - - this.time = makeElement('input', { - type: 'time', - disabled: this.dateonly - }) as HTMLInputElement - } - - connectedCallback() { - /* This can be in the constructor for chromium, but NOT firefox... - Vivaldi 4.3.2439.63 stable - Mozilla Firefox 94.0.1 - */ - /* - 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(this.date, this.time) - } - - static get observedAttributes() { - return ['dateonly'] - } - - attributeChangedCallback(name: string, _: string | null, to: string | null): void { - switch (name) { - case 'dateonly': - if (to == null) { - this.time.disabled = false - } else { - if (to == '' || to == name) { - this.time.disabled = true; - } else { - throw new TypeError(`Invalid value for attribute dateonly: ${to}`) - } - } - break; - } - } - - get dateonly(): boolean { - return this.hasAttribute('dateonly'); - } - - set dateonly(b: boolean) { - if (b) { - this.setAttribute('dateonly', ""); - } else { - this.removeAttribute('dateonly'); - } - } - - set value(date: Date) { - let [d, t] = date.format("~L~Y-~m-~dT~H:~M").split('T'); - this.date.value = d; - this.time.value = t; - - this.dateonly = date.dateonly; - } - - get value(): Date { - let dt; - let date = this.date.value; - if (this.dateonly) { - dt = parseDate(date); - dt.dateonly = true; - } else { - let time = this.time.value; - dt = parseDate(date + 'T' + time) - dt.dateonly = false; - } - return dt; - } - - get stringValue(): string { - if (this.dateonly) { - return this.value.format("~Y-~m-~d") - } else { - return this.value.format("~Y-~m-~dT~H:~M:~S") - } - } - - set stringValue(new_value: Date | string) { - 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.date.value = date; - this.time.value = time; - } - - addEventListener(type: string, proc: ((e: Event) => void)) { - if (type != 'input') throw "Only input supported"; - - this.date.addEventListener(type, proc); - this.time.addEventListener(type, proc); - } -} diff --git a/static/components/edit-rrule.ts b/static/components/edit-rrule.ts deleted file mode 100644 index a361bdee..00000000 --- a/static/components/edit-rrule.ts +++ /dev/null @@ -1,75 +0,0 @@ -export { EditRRule } - -import { ComponentVEvent } from './vevent' -import { VEvent } from '../vevent' -import { vcal_objects } from '../globals' - -import { RecurrenceRule } from '../vevent' - -/* <vevent-edit-rrule/> - Tab for editing the recurrence rule of a component -*/ -class EditRRule extends ComponentVEvent { - - constructor(uid?: string) { - super(uid); - - if (!this.template) { - throw 'vevent-edit-rrule template required'; - } - - let frag = this.template.content.cloneNode(true) as DocumentFragment - let body = frag.firstElementChild! - this.replaceChildren(body); - - for (let el of this.querySelectorAll('[name]')) { - el.addEventListener('input', () => { - // console.log(this); - let data = vcal_objects.get(this.uid)!; - let rrule = data.getProperty('rrule') - if (!rrule) { - console.warn('RRUle missing from object'); - return; - } - rrule = rrule as RecurrenceRule - - console.log(el.getAttribute('name'), (el as any).value); - rrule[el.getAttribute('name')!] = (el as any).value; - data.setProperty('rrule', rrule); - - }); - } - } - - 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: - <input/>, <input-list/>, <select/>, and <date-time-input/> - Which all have `name` and `value` fields, allowing the code - below to work. - */ - - let name = el.getAttribute('name') - if (!name) { - console.warn(`Input without name, ${el}`) - continue - } - - let value: any = rrule[name]; - if (value) - (el as any).value = value; - } - } - -} diff --git a/static/components/input-list.ts b/static/components/input-list.ts deleted file mode 100644 index 0afd4999..00000000 --- a/static/components/input-list.ts +++ /dev/null @@ -1,120 +0,0 @@ -export { InputList } - -/* - TODO allow each item to be a larger unit, possibly containing multiple input - fields. -*/ -class InputList extends HTMLElement { - - el: HTMLInputElement; - - #listeners: [string, (e: Event) => void][] = []; - - constructor() { - super(); - this.el = this.children[0].cloneNode(true) as HTMLInputElement; - } - - connectedCallback() { - for (let child of this.children) { - child.remove(); - } - this.addInstance(); - } - - createInstance(): HTMLInputElement { - let new_el = this.el.cloneNode(true) as HTMLInputElement - let that = this; - new_el.addEventListener('input', function() { - /* TODO .value is empty both if it's actually empty, but also - for invalid input. Check new_el.validity, and new_el.validationMessage - */ - if (new_el.value === '') { - let sibling = (this.previousElementSibling || this.nextElementSibling) - /* Only remove ourselves if we have siblings - Otherwise we just linger */ - if (sibling) { - this.remove(); - (sibling as HTMLInputElement).focus(); - } - } else { - if (!this.nextElementSibling) { - that.addInstance(); - // window.setTimeout(() => this.focus()) - this.focus(); - } - } - }); - - for (let [type, proc] of this.#listeners) { - new_el.addEventListener(type, proc); - } - - return new_el; - } - - addInstance() { - let new_el = this.createInstance(); - this.appendChild(new_el); - } - - get value(): any[] { - let value_list = [] - for (let child of this.children) { - value_list.push((child as any).value); - } - if (value_list[value_list.length - 1] === '') { - value_list.pop(); - } - return value_list - } - - set value(new_value: any[]) { - - let all_equal = true; - for (let i = 0; i < this.children.length; i++) { - let sv = (this.children[i] as any).value - all_equal - &&= (sv == new_value[i]) - || (sv === '' && new_value[i] == undefined) - } - if (all_equal) return; - - /* Copy our current input elements into a dictionary. - This allows us to only create new elements where needed - */ - let values = new Map; - for (let child of this.children) { - values.set((child as HTMLInputElement).value, child); - } - - let output_list: HTMLInputElement[] = [] - for (let value of new_value) { - let element; - /* Only create element if needed */ - if ((element = values.get(value))) { - output_list.push(element) - /* clear dictionary */ - values.set(value, false); - } else { - let new_el = this.createInstance(); - new_el.value = value; - output_list.push(new_el); - } - } - /* final, trailing, element */ - output_list.push(this.createInstance()); - - this.replaceChildren(...output_list); - } - - addEventListener(type: string, proc: ((e: Event) => void)) { - // if (type != 'input') throw "Only input supported"; - - this.#listeners.push([type, proc]) - - for (let child of this.children) { - child.addEventListener(type, proc); - } - } -} diff --git a/static/components/popup-element.ts b/static/components/popup-element.ts deleted file mode 100644 index 458f543c..00000000 --- a/static/components/popup-element.ts +++ /dev/null @@ -1,201 +0,0 @@ -export { PopupElement, setup_popup_element } - -import { VEvent } from '../vevent' -import { find_block, vcal_objects } from '../globals' - -import { ComponentVEvent } from './vevent' - -import { remove_event } from '../server_connect' - -/* <popup-element /> */ -class PopupElement extends ComponentVEvent { - - /* The popup which is the "selected" popup. - /* Makes the popup last hovered over the selected popup, moving it to - * the top, and allowing global keyboard bindings to affect it. */ - static activePopup: PopupElement | null = null; - - constructor(uid?: string) { - super(uid); - - /* TODO populate remaining (??) */ - - let obj = vcal_objects.get(this.uid); - if (obj && obj.calendar) { - this.dataset.calendar = obj.calendar; - } - - /* Makes us the active popup */ - this.addEventListener('mouseover', () => { - if (PopupElement.activePopup) { - PopupElement.activePopup.removeAttribute('active'); - } - PopupElement.activePopup = this; - this.setAttribute('active', 'active'); - }) - } - - redraw(data: VEvent) { - if (data.calendar) { - /* The CSS has hooks on [data-calendar], meaning that this can - (and will) change stuff */ - this.dataset.calendar = data.calendar; - } - - } - - connectedCallback() { - let template = document.getElementById('popup-template') as HTMLTemplateElement - let body = (template.content.cloneNode(true) as DocumentFragment).firstElementChild!; - - let uid = this.uid; - - /* nav bar */ - let nav = body.getElementsByClassName("popup-control")[0] as HTMLElement; - bind_popup_control(nav); - - let close_btn = body.querySelector('.popup-control .close-button') as HTMLButtonElement - close_btn.addEventListener('click', () => this.visible = false); - - let maximize_btn = body.querySelector('.popup-control .maximize-button') as HTMLButtonElement - maximize_btn.addEventListener('click', () => this.maximize()); - - let remove_btn = body.querySelector('.popup-control .remove-button') as HTMLButtonElement - remove_btn.addEventListener('click', () => remove_event(uid)); - /* end nav bar */ - - this.replaceChildren(body); - } - - static get observedAttributes() { - return ['visible']; - } - - attributeChangedCallback(name: string, _?: string, newValue?: string) { - switch (name) { - case 'visible': - if (newValue !== null) - /* Only run resize code when showing the popup */ - this.onVisibilityChange() - break; - } - } - - get visible(): boolean { - return this.hasAttribute('visible'); - } - - set visible(isVisible: boolean) { - if (isVisible) { - this.setAttribute('visible', 'visible'); - } else { - this.removeAttribute('visible'); - } - } - - private onVisibilityChange() { - console.log('here'); - - /* TODO better way to find root */ - let root; - switch (window.VIEW) { - case 'week': - root = document.getElementsByClassName("days")[0]; - break; - case 'month': - default: - root = document.body; - break; - } - - let element = find_block(this.uid) as HTMLElement | null - /* start <X, Y> 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"; - - /* Reset width and height to initial, to save user if they have resized - it to something weird */ - let el = this.firstElementChild as HTMLElement; - el.style.removeProperty('width'); - el.style.removeProperty('height'); - } - - maximize() { - /* TODO this assumes that popups are direct decendant of their parent, - which they really ought to be */ - let parent = this.parentElement!; - let el = this.firstElementChild as HTMLElement - /* TODO offsetParent.scrollLeft places us "fullscreen" according to the currently - scrolled viewport. But is this the correct way to do it? How does it work for - month views */ - this.style.left = `${this.offsetParent!.scrollLeft + 10}px`; - this.style.top = '10px'; - /* 5ex is width of tab labels */ - el.style.width = `calc(${parent.clientWidth - 20}px - 5ex)` - el.style.height = `${parent.clientHeight - 20}px` - } -} - -/* Create a new popup element for the given VEvent, and ready it for editing the - event. Used when creating event (through the frontend). - The return value can safely be ignored. -*/ -function setup_popup_element(ev: VEvent): PopupElement { - let uid = ev.getProperty('uid'); - let popup = new PopupElement(uid); - ev.register(popup); - /* TODO propper way to find popup container */ - (document.querySelector('.days') as Element).appendChild(popup); - let tabBtn = popup.querySelector('[role="tab"][data-originaltitle="Edit"]') as HTMLButtonElement - tabBtn.click() - let tab = document.getElementById(tabBtn.getAttribute('aria-controls')!)! - let input = tab.querySelector('input[name="summary"]') as HTMLInputElement - popup.visible = true; - input.select(); - return popup; -} - -/* - Given the navbar of a popup, make it dragable. - */ -function bind_popup_control(nav: HTMLElement) { - - // if (!nav.closest('popup-element')) { - // console.log(nav); - // throw TypeError('not a popup container'); - // } - - nav.addEventListener('mousedown', function(e) { - /* Ignore mousedown on children */ - if (e.target != nav) return; - nav.style.cursor = "grabbing"; - nav.dataset.grabbed = "true"; - nav.dataset.grabPoint = e.clientX + ";" + e.clientY; - // let popup = nav.closest(".popup-container"); - let popup = nav.closest("popup-element") as HTMLElement; - nav.dataset.startPoint = popup.offsetLeft + ";" + popup.offsetTop; - }) - window.addEventListener('mousemove', function(e) { - if (nav.dataset.grabbed) { - let [x, y] = nav.dataset.grabPoint!.split(";").map(Number); - let [startX, startY] = nav.dataset.startPoint!.split(";").map(Number); - // let popup = nav.closest(".popup-container"); - let popup = nav.closest("popup-element") as HTMLElement; - - popup.style.left = startX + (e.clientX - x) + "px"; - popup.style.top = startY + (e.clientY - y) + "px"; - } - }); - window.addEventListener('mouseup', function() { - nav.dataset.grabbed = ""; - nav.style.cursor = ""; - }); -} diff --git a/static/components/slider.ts b/static/components/slider.ts deleted file mode 100644 index 48abc91b..00000000 --- a/static/components/slider.ts +++ /dev/null @@ -1,101 +0,0 @@ -export { SliderInput } - -import { makeElement } from '../lib' - -const dflt = { - min: 0, - max: 100, - step: 1, -} - -type Attribute = 'min' | 'max' | 'step' - -class SliderInput extends HTMLElement { - - /* value a string since javascript kind of expects that */ - #value = "0"; - min = 0; - max = 100; - step = 1; - - readonly slider: HTMLInputElement; - readonly textIn: HTMLInputElement; - - constructor(min?: number, max?: number, step?: number, value?: number) { - super(); - - this.min = min || parseFloat(this.getAttribute('min') || "" + dflt['min']); - this.max = max || parseFloat(this.getAttribute('max') || "" + dflt['max']); - this.step = step || parseFloat(this.getAttribute('step') || "" + dflt['step']); - // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/range#value - const defaultValue - = (this.max < this.min) - ? this.min - : this.min + (this.max - this.min) / 2; - - this.slider = makeElement('input', { - type: 'range', - min: this.min, - max: this.max, - step: this.step, - value: this.value, - }) as HTMLInputElement - this.textIn = makeElement('input', { - type: 'number', - min: this.min, - max: this.max, - step: this.step, - value: this.value, - }) as HTMLInputElement - - this.slider.addEventListener('input', e => this.propagate(e)); - this.textIn.addEventListener('input', e => this.propagate(e)); - - /* MUST be after sub components are bound */ - this.value = "" + (value || this.getAttribute('value') || defaultValue); - } - - connectedCallback() { - this.replaceChildren(this.slider, this.textIn); - } - - - static get observedAttributes(): Attribute[] { - return ['min', 'max', 'step'] - } - - attributeChangedCallback(name: Attribute, _?: string, to?: string): void { - if (to) { - this.slider.setAttribute(name, to); - this.textIn.setAttribute(name, to); - } else { - this.slider.removeAttribute(name); - this.textIn.removeAttribute(name); - } - this[name] = parseFloat(to || "" + dflt[name]) - } - - propagate(e: Event) { - this.value = (e.target as HTMLInputElement).value; - if (e instanceof InputEvent && this.oninput) { - this.oninput(e); - } - } - - set value(value: string) { - this.slider.value = value; - this.textIn.value = value; - this.#value = value; - } - - get value(): string { - return this.#value; - } - - /* TODO do we want to implement this? - * oninput directly on the component already works - * / - addEventListener(type: string, proc: ((e: Event) => void)) { - } - */ -} diff --git a/static/components/tab-group-element.ts b/static/components/tab-group-element.ts deleted file mode 100644 index e90997e9..00000000 --- a/static/components/tab-group-element.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { ComponentVEvent } from './vevent' -import { makeElement, gensym } from '../lib' -import { EditRRule } from './edit-rrule' -import { VEvent, isRedrawable } from '../vevent' -import { vcal_objects } from '../globals' - -export { TabGroupElement } - -/* Lacks a template, since it's trivial - The initial children of this element all becomes tabs, each child may have - the datapropertys 'label' and 'title' set, where label is what is shown in - the tab bar, and title is the hower text. - - All additions and removals of tabs MUST go through addTab and removeTab! - - Information about how tabs should work from an accesability standpoint can be - found here: - https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/Tab_Role - - <tab-group/> -*/ -class TabGroupElement extends ComponentVEvent { - - readonly menu: HTMLElement; - - tabs: HTMLElement[] = []; - tabLabels: HTMLElement[] = []; - - constructor(uid?: string) { - super(uid); - - this.menu = makeElement('div', {}, { - role: 'tablist', - 'aria-label': 'Simple Tabs', - }) - } - - redraw(data: VEvent) { - /* Update our tabset to match data:s having or not having of rrule, - but do nothing if we already match */ - let rrule_tab = this.has_rrule_tab() - if (data.getProperty('rrule')) { - if (!this.has_rrule_tab()) { - /* Note that EditRRule register itself to be updated on changes - to the event */ - this.addTab(new EditRRule(data.getProperty('uid')), - "↺", "Upprepningar"); - } - } else { - if (rrule_tab) this.removeTab(rrule_tab as HTMLElement); - } - - /* TODO is there any case where we want to propagate the draw to any of - our tabs? or are all our tabs independent? */ - } - - connectedCallback() { - /* All pre-added children should become tabs, but start with removing - them and storing them for later */ - let originalChildren: HTMLElement[] = []; - while (this.firstChild) { - originalChildren.push(this.removeChild(this.firstChild) as HTMLElement); - } - - /* Add our tab label menu */ - this.appendChild(this.menu); - - /* Re-add our initial children, but as proper tab elements */ - for (let child of originalChildren) { - this.addTab(child); - } - - /* redraw might add or remove tabs depending on our data, so call it here */ - this.redraw(vcal_objects.get(this.uid)!); - - /* All tabs should now be ready, focus the first one */ - if (this.tabLabels.length > 0) { - this.tabLabels[0].setAttribute('tabindex', '0'); - this.tabLabels[0].click(); - } - - } /* end connectedCallback */ - - addTab(child: HTMLElement, label?: string, title?: string) { - - /* First character of text is a good a guess as any for our label, - but still defaut to '?' if no text is found */ - label = label || child.dataset.label || (child.textContent + '?')[0]; - title = title || child.dataset.title || ''; - let extra_attributes = {}; - /* Used to target a tab by name */ - if (child.dataset.originaltitle) { - extra_attributes = { 'data-originaltitle': child.dataset.originaltitle } - } - - let tab_id = gensym('tab_content_'); - let label_id = gensym('tab_label_'); - - let tabLabel = makeElement('button', { - textContent: label, - }, { - role: 'tab', - id: label_id, - tabindex: -1, - title: title, - 'aria-selected': false, - 'aria-controls': tab_id, - ...extra_attributes, - }) - - let tabContainer = makeElement('div', {}, { - id: tab_id, - role: 'tabpanel', - tabindex: 0, - hidden: 'hidden', - 'aria-labelledby': label_id, - }) - - tabContainer.replaceChildren(child); - this.tabs.push(tabContainer); - this.appendChild(tabContainer); - - this.tabLabels.push(tabLabel); - this.menu.appendChild(tabLabel); - - tabLabel.addEventListener('click', () => this.tabClickedCallback(tabLabel)); - - this.style.setProperty('--tabcount', '' + this.tabs.length); - } - - removeTab(tab: HTMLElement) { - let id = tab.getAttribute('aria-labelledby')! - let label = document.getElementById(id) - if (label) { - if (label.ariaSelected === 'true') { - this.tabLabels[0].click(); - } - this.tabLabels = this.tabLabels.filter(el => el !== label) - label.remove(); - } - /* remove tab */ - this.tabs = this.tabs.filter(el => el !== tab) - this.removeChild(tab); - if (tab.firstChild) { - let child = tab.firstChild as HTMLElement; - if (isRedrawable(child)) { - vcal_objects.get(this.uid)?.unregister(child) - } - } - - this.style.setProperty('--tabcount', '' + this.tabs.length); - } - - /* TODO replace querySelectors here with our already saved references */ - tabClickedCallback(tab: Element) { - - /* hide all tab panels */ - for (let tabcontent of this.querySelectorAll('[role="tabpanel"]')) { - tabcontent.setAttribute('hidden', 'hidden'); - } - /* unselect all (selected) tab handles */ - for (let item of this.querySelectorAll('[aria-selected="true"]')) { - item.setAttribute('aria-selected', 'false'); - } - /* re-select ourselves */ - tab.setAttribute('aria-selected', 'true'); - - /* unhide our target tab */ - this.querySelector('#' + tab.getAttribute('aria-controls'))! - .removeAttribute('hidden') - } - - - /* returns our rrule tab if we have one */ - has_rrule_tab(): Element | false { - for (let child of this.children) { - if (child.firstChild! instanceof EditRRule) { - return child; - } - } - return false; - } - -} diff --git a/static/components/vevent-block.ts b/static/components/vevent-block.ts deleted file mode 100644 index 9bbb8e7e..00000000 --- a/static/components/vevent-block.ts +++ /dev/null @@ -1,99 +0,0 @@ -export { ComponentBlock } - -import { ComponentVEvent } from './vevent' -import { VEvent } from '../vevent' -import { parseDate, to_local } from '../lib' - - -/* <vevent-block /> - - A grahpical block in the week view. -*/ -class ComponentBlock extends ComponentVEvent { - constructor(uid?: string) { - super(uid); - - if (!this.template) { - throw 'vevent-block template required'; - } - - this.addEventListener('click', () => { - let uid = this.uid - /* TODO is it better to find the popup through a query selector, or - by looking through all registered components of a VEvent? */ - let popup = document.querySelector(`popup-element[data-uid="${uid}"]`) - if (popup === null) throw new Error('no popup for uid ' + uid); - popup.toggleAttribute('visible'); - }); - } - - redraw(data: VEvent) { - let body = (this.template!.content.cloneNode(true) as DocumentFragment).firstElementChild!; - - for (let el of body.querySelectorAll('[data-property]')) { - if (!(el instanceof HTMLElement)) continue; - let p = el.dataset.property!; - let d, fmt; - if ((d = data.getProperty(p))) { - if ((fmt = el.dataset.fmt)) { - el.textContent = d.format(fmt); - } else { - el.textContent = d; - } - } else switch (p.toLowerCase()) { - /* We lack that property, but might want to set a default here */ - case 'summary': - el.textContent = 'Ny händelse' - break; - } - } - - this.replaceChildren(body); - - /* -------------------------------------------------- */ - - if (window.VIEW === 'week') { - 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; - } - - if (data.getProperty('rrule') !== undefined) { - let rep = this.getElementsByClassName('repeating') - if (rep.length !== 0) { - (rep[0] as HTMLElement).innerText = '↺' - } - } - } -} diff --git a/static/components/vevent-description.ts b/static/components/vevent-description.ts deleted file mode 100644 index b44185e7..00000000 --- a/static/components/vevent-description.ts +++ /dev/null @@ -1,38 +0,0 @@ -export { ComponentDescription } - -import { VEvent } from '../vevent' -import { ComponentVEvent } from './vevent' -import { format } from '../formatters' - -/* - <vevent-description /> -*/ -class ComponentDescription extends ComponentVEvent { - - constructor(uid?: string) { - super(uid); - if (!this.template) { - throw 'vevent-description template required'; - } - } - - redraw(data: VEvent) { - // update ourselves from template - - let body = (this.template!.content.cloneNode(true) as DocumentFragment).firstElementChild!; - - for (let el of body.querySelectorAll('[data-property]')) { - if (!(el instanceof HTMLElement)) continue; - format(el, data, el.dataset.property!); - } - - let repeating = body.getElementsByClassName('repeating')[0] as HTMLElement - if (data.getProperty('rrule')) { - repeating.classList.remove('hidden'); - } else { - repeating.classList.add('hidden'); - } - - this.replaceChildren(body); - } -} diff --git a/static/components/vevent-dl.ts b/static/components/vevent-dl.ts deleted file mode 100644 index a792c07f..00000000 --- a/static/components/vevent-dl.ts +++ /dev/null @@ -1,35 +0,0 @@ -export { VEventDL } - -import { ComponentVEvent } from './vevent' -import { VEvent } from '../vevent' -import { makeElement } from '../lib' - -import { RecurrenceRule } from '../vevent' - -/* <vevent-dl /> */ -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', { textContent: key })) - let fmtVal: string = val; - if (val instanceof Date) { - fmtVal = val.format( - 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', { textContent: fmtVal })) - } - return dl; -} diff --git a/static/components/vevent-edit.ts b/static/components/vevent-edit.ts deleted file mode 100644 index e3b5d105..00000000 --- a/static/components/vevent-edit.ts +++ /dev/null @@ -1,179 +0,0 @@ -export { ComponentEdit } - -import { ComponentVEvent } from './vevent' -import { InputList } from './input-list' -import { DateTimeInput } from './date-time-input' - -import { vcal_objects } from '../globals' -import { VEvent, RecurrenceRule } from '../vevent' -import { create_event } from '../server_connect' -import { to_boolean, gensym } from '../lib' - -/* <vevent-edit /> - Edit form for a given VEvent. Used as the edit tab of popups. -*/ -class ComponentEdit extends ComponentVEvent { - - constructor(uid?: string) { - super(uid); - - if (!this.template) { - throw 'vevent-edit template required'; - } - - let frag = this.template.content.cloneNode(true) as DocumentFragment - let body = frag.firstElementChild! - this.replaceChildren(body); - - let data = vcal_objects.get(this.uid) - if (!data) { - throw `Data missing for uid ${this.dataset.uid}.` - } - - for (let el of this.querySelectorAll('[data-label]')) { - let label = document.createElement('label'); - let id = el.id || gensym('input'); - el.id = id; - label.htmlFor = id; - label.textContent = (el as HTMLElement).dataset.label!; - el.parentElement!.insertBefore(label, el); - } - - /* Handle calendar dropdown */ - for (let el of this.querySelectorAll('select.calendar-selection')) { - for (let opt of el.getElementsByTagName('option')) { - opt.selected = false; - } - if (data.calendar) { - (el as HTMLSelectElement).value = data.calendar; - } - - el.addEventListener('change', e => { - let v = (e.target as HTMLSelectElement).selectedOptions[0].value - let obj = vcal_objects.get(this.uid)! - obj.calendar = v; - }); - } - - - // 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) - if (obj === undefined) { - throw 'No object with uid ' + this.uid - } - if (!(el instanceof HTMLInputElement - || el instanceof DateTimeInput - || el instanceof HTMLTextAreaElement - || el instanceof InputList - )) { - console.log(el, 'not an HTMLInputElement'); - return; - } - obj.setProperty( - el.dataset.property!, - el.value) - }); - } - - 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 has_repeats_ = this.querySelector('[name="has_repeats"]') - if (has_repeats_) { - let has_repeats = has_repeats_ as HTMLInputElement; - - has_repeats.addEventListener('click', () => { - /* TODO unselecting and reselecting this checkbox deletes all entered data. - Cache it somewhere */ - if (has_repeats.checked) { - vcal_objects.get(this.uid)!.setProperty('rrule', new RecurrenceRule()) - } else { - /* TODO is this a good way to remove a property ? */ - vcal_objects.get(this.uid)!.setProperty('rrule', undefined) - } - }) - } - - 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; - }); - } - - 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}.` - } - - this.redraw(data); - - // return; - } - - redraw(data: VEvent) { - /* We only update our fields, instead of reinstansiating - ourselves from the template, in hope that it's faster */ - - - for (let el of this.querySelectorAll("[data-property]")) { - 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; - }); - } - } - - let el = this.querySelector('[name="has_repeats"]') - if (el) { - (el as HTMLInputElement).checked = to_boolean(data.getProperty('rrule')) - } - - if (data.calendar) { - for (let el of this.getElementsByClassName('calendar-selection')) { - (el as HTMLSelectElement).value = data.calendar; - } - } - } -} diff --git a/static/components/vevent.ts b/static/components/vevent.ts deleted file mode 100644 index 7487cbb6..00000000 --- a/static/components/vevent.ts +++ /dev/null @@ -1,69 +0,0 @@ -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. -*/ -abstract class ComponentVEvent extends HTMLElement { - - template?: HTMLTemplateElement - uid: string - - constructor(uid?: string) { - super(); - this.template = document.getElementById(this.tagName.toLowerCase()) as HTMLTemplateElement | undefined - - let real_uid; - - if (uid) { - // console.log('Got UID directly'); - real_uid = uid; - } else { - /* I know that this case is redundant, it's here if we don't want to - look up the tree later */ - if (this.dataset.uid) { - // console.log('Had UID as direct attribute'); - real_uid = this.dataset.uid; - } else { - let el = this.closest('[data-uid]') - if (el) { - // console.log('Found UID higher up in the tree'); - real_uid = (el as HTMLElement).dataset.uid - } else { - throw "No parent with [data-uid] set" - } - } - } - - if (!real_uid) { - console.warn(this.outerHTML); - throw `UID required` - } - - // console.log(real_uid); - this.uid = real_uid; - this.dataset.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 = this.dataset.uid - if (uid) { - let v = vcal_objects.get(uid) - if (v) this.redraw(v); - } - } - - abstract redraw(data: VEvent): void - -} |