aboutsummaryrefslogtreecommitdiff
path: root/static/components
diff options
context:
space:
mode:
authorHugo Hörnquist <hugo@lysator.liu.se>2021-11-30 01:09:53 +0100
committerHugo Hörnquist <hugo@lysator.liu.se>2021-11-30 01:09:53 +0100
commite71f0c20adc4dc2f49bca99a859241fdadf376d3 (patch)
tree3231d1e491fcd1ed9dc0ab31cac4fdedb733807d /static/components
parentChange UID resolve. (diff)
downloadcalp-e71f0c20adc4dc2f49bca99a859241fdadf376d3.tar.gz
calp-e71f0c20adc4dc2f49bca99a859241fdadf376d3.tar.xz
Rework tab system.
This sepparates popup-elements from their tabbed contents, allowing clearer sepparations of concerns, along with easier adding and removing of tabs to the tabset!
Diffstat (limited to 'static/components')
-rw-r--r--static/components/date-time-input.ts2
-rw-r--r--static/components/edit-rrule.ts8
-rw-r--r--static/components/popup-element.ts64
-rw-r--r--static/components/tab-group-element.ts177
-rw-r--r--static/components/vevent-edit.ts22
-rw-r--r--static/components/vevent.ts9
6 files changed, 210 insertions, 72 deletions
diff --git a/static/components/date-time-input.ts b/static/components/date-time-input.ts
index 27dad095..d42c5523 100644
--- a/static/components/date-time-input.ts
+++ b/static/components/date-time-input.ts
@@ -61,7 +61,7 @@ class DateTimeInput extends /* HTMLInputElement */ HTMLElement {
set value(date: Date) {
let [d, t] = date.format("~L~Y-~m-~dT~H:~M:~S").split('T');
- console.log(d, t);
+ // console.log(d, t);
(this.querySelector("input[type='date']") as HTMLInputElement).value = d;
(this.querySelector("input[type='time']") as HTMLInputElement).value = t;
diff --git a/static/components/edit-rrule.ts b/static/components/edit-rrule.ts
index a4d09083..6be01b76 100644
--- a/static/components/edit-rrule.ts
+++ b/static/components/edit-rrule.ts
@@ -6,11 +6,13 @@ import { vcal_objects } from '../globals'
import { RecurrenceRule } from '../vevent'
-/* <vevent-edit-rrule/> */
+/* <vevent-edit-rrule/>
+ Tab for editing the recurrence rule of a component
+*/
class EditRRule extends ComponentVEvent {
- constructor() {
- super();
+ constructor(uid?: string) {
+ super(uid);
let frag = this.template.content.cloneNode(true) as DocumentFragment
let body = frag.firstElementChild!
diff --git a/static/components/popup-element.ts b/static/components/popup-element.ts
index f4b934d8..1a57032b 100644
--- a/static/components/popup-element.ts
+++ b/static/components/popup-element.ts
@@ -1,6 +1,5 @@
export { PopupElement }
-import { gensym } from '../lib'
import { VEvent } from '../vevent'
import { bind_popup_control } from '../dragable'
import { close_popup, event_from_popup } from '../popup'
@@ -13,9 +12,6 @@ import { remove_event } from '../server_connect'
/* <popup-element /> */
class PopupElement extends ComponentVEvent {
- tabgroup_id: string
- tabcount: number
-
isVisible: boolean = false;
constructor(uid?: string) {
@@ -23,9 +19,6 @@ class PopupElement extends ComponentVEvent {
/* TODO populate remaining (??) */
- this.tabgroup_id = gensym();
- this.tabcount = 0
-
let obj = vcal_objects.get(this.uid);
if (obj && obj.calendar) {
this.dataset.calendar = obj.calendar;
@@ -34,70 +27,19 @@ class PopupElement extends ComponentVEvent {
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;
}
- /* 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 template = document.getElementById('popup-template') as HTMLTemplateElement
let body = (template.content.cloneNode(true) as DocumentFragment).firstElementChild!;
let uid = this.uid;
- window.setTimeout(() => {
-
- /* tab change button */
- let tabs = this.querySelectorAll('[role="tab"]')
- /* list of all tabs */
- // let tablist = this.querySelector('[role="tablist"]')!
-
- tabs.forEach(tab => {
- tab.addEventListener('click', () => {
-
- /* hide all tab panels */
- for (let tabcontent of this.querySelectorAll('[role="tabpanel"]')) {
- tabcontent.setAttribute('hidden', 'true');
- }
- /* 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')
- });
- });
-
- /* tab contents */
- let tabcontents = this.querySelectorAll('[role="tabpanel"]')
-
- for (let i = 0; i < tabs.length; i++) {
- let n = i + this.tabcount;
- this.tabgroup_id
- let tab = tabs[n];
- let con = tabcontents[n];
-
- let a = `${this.tabgroup_id}-tab-${n}`
- let b = `${this.tabgroup_id}-con-${n}`
-
- tab.id = a;
- con.setAttribute('aria-labeledby', a);
-
- con.id = b;
- tab.setAttribute('aria-controls', b);
-
- }
- this.tabcount += tabs.length
-
- });
- /* end tabs */
-
/* nav bar */
let nav = body.getElementsByClassName("popup-control")[0] as HTMLElement;
bind_popup_control(nav);
diff --git a/static/components/tab-group-element.ts b/static/components/tab-group-element.ts
new file mode 100644
index 00000000..dc97df93
--- /dev/null
+++ b/static/components/tab-group-element.ts
@@ -0,0 +1,177 @@
+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 {
+
+ menu: HTMLElement;
+
+ tabs: HTMLElement[] = [];
+ tabLabels: HTMLElement[] = [];
+
+ constructor(uid?: string) {
+ super(uid);
+
+ this.menu = makeElement('menu', {}, {
+ 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 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,
+ })
+
+ let tabContainer = makeElement('article', {}, {
+ id: tab_id,
+ role: 'tabpanel',
+ tabindex: 0,
+ hidden: 'hidden',
+ 'aria-labeledby': 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-labeledby')!
+ 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', 'true');
+ }
+ /* 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')
+ }
+
+
+ has_rrule_tab(): Element | false {
+ for (let child of this.children) {
+ if ((child.firstChild! as HTMLElement).tagName.toLowerCase() === 'vevent-edit-rrule') {
+ return child;
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/static/components/vevent-edit.ts b/static/components/vevent-edit.ts
index b9b733a0..58cee870 100644
--- a/static/components/vevent-edit.ts
+++ b/static/components/vevent-edit.ts
@@ -5,7 +5,7 @@ import { InputList } from './input-list'
import { DateTimeInput } from './date-time-input'
import { vcal_objects } from '../globals'
-import { VEvent } from '../vevent'
+import { VEvent, RecurrenceRule } from '../vevent'
import { create_event } from '../server_connect'
/* <vevent-edit />
@@ -13,8 +13,8 @@ import { create_event } from '../server_connect'
*/
class ComponentEdit extends ComponentVEvent {
- constructor() {
- super();
+ constructor(uid?: string) {
+ super(uid);
let frag = this.template.content.cloneNode(true) as DocumentFragment
let body = frag.firstElementChild!
@@ -96,6 +96,22 @@ class ComponentEdit extends ComponentVEvent {
});
}
+ 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);
diff --git a/static/components/vevent.ts b/static/components/vevent.ts
index a7fe3e08..01391f9e 100644
--- a/static/components/vevent.ts
+++ b/static/components/vevent.ts
@@ -20,20 +20,20 @@ abstract class ComponentVEvent extends HTMLElement {
let real_uid;
- console.log(this.tagName);
+ // console.log(this.tagName);
if (uid) {
- console.log('Got UID directly');
+ // 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');
+ // 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');
+ // console.log('Found UID higher up in the tree');
real_uid = (el as HTMLElement).dataset.uid
} else {
throw "No parent with [data-uid] set"
@@ -46,6 +46,7 @@ abstract class ComponentVEvent extends HTMLElement {
throw `UID required`
}
+ // console.log(real_uid);
this.uid = real_uid;
this.dataset.uid = real_uid;