From e71f0c20adc4dc2f49bca99a859241fdadf376d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Tue, 30 Nov 2021 01:09:53 +0100 Subject: 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! --- static/components/date-time-input.ts | 2 +- static/components/edit-rrule.ts | 8 +- static/components/popup-element.ts | 64 +----------- static/components/tab-group-element.ts | 177 +++++++++++++++++++++++++++++++++ static/components/vevent-edit.ts | 22 +++- static/components/vevent.ts | 9 +- static/elements.ts | 2 + static/lib.ts | 5 +- static/style.scss | 34 +++++-- static/vevent.ts | 12 ++- 10 files changed, 253 insertions(+), 82 deletions(-) create mode 100644 static/components/tab-group-element.ts (limited to 'static') 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' -/* */ +/* + 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' /* */ 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 + + +*/ +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' /* @@ -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; diff --git a/static/elements.ts b/static/elements.ts index b499556f..870a27e6 100644 --- a/static/elements.ts +++ b/static/elements.ts @@ -6,6 +6,7 @@ import { DateTimeInput } from './components/date-time-input' import { PopupElement } from './components/popup-element' import { InputList } from './components/input-list' import { EditRRule } from './components/edit-rrule' +import { TabGroupElement } from './components/tab-group-element' export { initialize_components } @@ -29,4 +30,5 @@ function initialize_components() { /* These maybe also require that the global maps are initialized */ customElements.define('popup-element', PopupElement) + customElements.define('tab-group', TabGroupElement) } diff --git a/static/lib.ts b/static/lib.ts index bc072545..2ef5b596 100644 --- a/static/lib.ts +++ b/static/lib.ts @@ -120,11 +120,14 @@ function to_local(date: Date): Date { /* -------------------------------------------------- */ -function makeElement(name: string, attr = {}): HTMLElement { +function makeElement(name: string, attr = {}, actualAttr = {}): HTMLElement { let element: HTMLElement = document.createElement(name); for (let [key, value] of Object.entries(attr)) { (element as any)[key] = value; } + for (let [key, value] of Object.entries(actualAttr)) { + element.setAttribute(key, '' + value); + } return element; } diff --git a/static/style.scss b/static/style.scss index bd29f4cc..e0a7e21e 100644 --- a/static/style.scss +++ b/static/style.scss @@ -782,20 +782,19 @@ popup-element { display: block; } - main { + > * { resize: both; + /* This overflow: auto gives us the correct resize handle */ overflow: auto; + /* TODO this doesn't work, since tabcount is sepparate fronm + * popup... */ min-height: calc(var(--tabcount) * #{$tablabel-margin + $tablabel-height}); /* some form of sensible minimi and default size for the popup (s main area). */ min-width: 150px; width: 350px; height: 250px; - - article { - padding: 1em; - } } } @@ -839,9 +838,6 @@ popup-element { } .popup-root { - background-color: #dedede; - color: black; - display: flex; @if $popup-style == "left" { @@ -849,7 +845,20 @@ popup-element { } @else { flex-direction: column; } +} + +tab-group { + background-color: #dedede; + color: black; + width: 100%; + height: 100%; + /* This overflow: auto gives us the correct rendering of the content */ + overflow: auto; + + [role="tabpanel"] { + padding: 1em; + } [role="tablist"] { display: flex; @@ -922,6 +931,15 @@ vevent-edit { } } +.checkboxes { + display: grid; + grid-template-rows: 1fr 1fr; + justify-items: baseline; + + label {grid-row: 1;} + input {grid-row: 2;} +} + vevent-dl { font-size: 80%; diff --git a/static/vevent.ts b/static/vevent.ts index e35c469f..4b6d44c6 100644 --- a/static/vevent.ts +++ b/static/vevent.ts @@ -3,7 +3,8 @@ import { parseDate } from './lib' export { VEvent, xml_to_vcal, - RecurrenceRule + RecurrenceRule, + isRedrawable, } /* Something which can be redrawn */ @@ -11,6 +12,11 @@ interface Redrawable extends HTMLElement { redraw: ((data: VEvent) => void) } +function isRedrawable(x: HTMLElement): x is Redrawable { + return 'redraw' in x +} + + class VEventValue { type: ical_type @@ -209,6 +215,10 @@ class VEvent { this.registered.push(htmlNode); } + unregister(htmlNode: Redrawable) { + this.registered = this.registered.filter(node => node !== htmlNode) + } + to_jcal(): JCal { let out_properties: JCalProperty[] = [] console.log(this.properties); -- cgit v1.2.3