aboutsummaryrefslogtreecommitdiff
path: root/static/components
diff options
context:
space:
mode:
authorHugo Hörnquist <hugo@lysator.liu.se>2021-11-10 01:40:22 +0100
committerHugo Hörnquist <hugo@lysator.liu.se>2021-11-10 01:40:22 +0100
commit410404cfdd54c083b6609fd52334e02d320145d7 (patch)
treeac934bde696f099590496d23bdd636f691f4c637 /static/components
parentBasic event modification works again. (diff)
downloadcalp-410404cfdd54c083b6609fd52334e02d320145d7.tar.gz
calp-410404cfdd54c083b6609fd52334e02d320145d7.tar.xz
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.
Diffstat (limited to 'static/components')
-rw-r--r--static/components/date-time-input.ts77
-rw-r--r--static/components/popup-element.ts100
-rw-r--r--static/components/tab-element.ts40
-rw-r--r--static/components/vevent-block.ts63
-rw-r--r--static/components/vevent-description.ts12
-rw-r--r--static/components/vevent-dl.ts24
-rw-r--r--static/components/vevent-edit.ts137
-rw-r--r--static/components/vevent.ts71
8 files changed, 524 insertions, 0 deletions
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'
+
+/* '<date-time-input />' */
+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 = '<input type="date" /><input type="time" />'
+ // 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'
+
+/* <popup-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 }
+
+/* <tab-element /> */
+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'
+
+
+/* <vevent-block />
+
+ 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'
+
+/*
+ <vevent-description />
+*/
+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'
+
+/* <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', { 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'
+
+/* <vevent-edit />
+ 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);
+ }
+
+}