From e753d721519f72014241b3d2fc804a919f655769 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Thu, 7 Sep 2023 02:58:41 +0200 Subject: Document remaining javascript items. --- static/ts/components/changelog.ts | 39 ++++++++++------- static/ts/components/date-jump.ts | 39 ++++++++++------- static/ts/components/date-time-input.ts | 38 +++++++++++++---- static/ts/components/input-list.ts | 70 ++++++++++++++++++++++++++----- static/ts/components/popup-element.ts | 6 ++- static/ts/components/slider.ts | 70 ++++++++++++++++++++++++++----- static/ts/components/tab-group-element.ts | 9 ++-- static/ts/components/vevent-block.ts | 12 ++++-- static/ts/components/vevent.ts | 19 +++++++++ 9 files changed, 237 insertions(+), 65 deletions(-) (limited to 'static/ts/components') diff --git a/static/ts/components/changelog.ts b/static/ts/components/changelog.ts index 720d1656..8f8adc1c 100644 --- a/static/ts/components/changelog.ts +++ b/static/ts/components/changelog.ts @@ -1,32 +1,43 @@ /** - * `` - * - * Display of a VEvents changelog. @ref{ChangeLogEntry} - * - * @privateRemarks @anchor{VEventChangelog} - * - * @category Web Components - * @mergeTarget components - * @module - */ + `` + + Display of a VEvents changelog. @ref{ChangeLogEntry} + + TODO rename this file! + + + @privateRemarks @anchor{VEventChangelog} + + @category Web Components + @mergeTarget components + @module +*/ import { makeElement } from '../lib' import { ComponentVEvent } from './vevent' import { VEvent } from '../vevent' export { VEventChangelog } +/** + Component displaying veevents changelog. + + This component is dumb, and (almost) doesn't keep any internal state. Instead + other parts of the program should call it with a `VEvent`, which contains the + actual changelog. +*/ class VEventChangelog extends ComponentVEvent { - readonly ul: HTMLElement + /** The list holding the changelog */ + readonly #ul: HTMLElement constructor(uid?: string) { super(uid); - this.ul = makeElement('ul'); + this.#ul = makeElement('ul'); } connectedCallback() { - this.replaceChildren(this.ul); + this.replaceChildren(this.#ul); } redraw(data: VEvent) { @@ -55,6 +66,6 @@ class VEventChangelog extends ComponentVEvent { children.push(makeElement('li', { textContent: msg })); } - this.ul.replaceChildren(...children) + this.#ul.replaceChildren(...children) } } diff --git a/static/ts/components/date-jump.ts b/static/ts/components/date-jump.ts index fd3908ae..f1cfe7e6 100644 --- a/static/ts/components/date-jump.ts +++ b/static/ts/components/date-jump.ts @@ -1,40 +1,51 @@ +/** + `` + + @category Web Components + @mergeTarget components + @module +*/ + export { DateJump } -/* Replace backend-driven [today] link with frontend, with one that +/** 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. + + TODO is this comment correct? We somehow contain an input element also. */ class DateJump extends HTMLElement { - readonly golink: HTMLAnchorElement; - readonly input: HTMLInputElement; + 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'; + this.#golink = document.createElement('a') + this.#golink.classList.add('btn'); + this.#golink.textContent = "➔" + this.#input = document.createElement('input') + this.#input.type = 'date'; } + /** Sets the link to NOW upon mounting */ connectedCallback() { /* Form is just here so the css works out */ let form = document.createElement('form'); - form.replaceChildren(this.input, this.golink); + 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` + 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; + this.#input.value = now; /* onchange isn't triggered by manually setting the value */ - this.golink.href = `${now}.html` + this.#golink.href = `${now}.html` } } diff --git a/static/ts/components/date-time-input.ts b/static/ts/components/date-time-input.ts index d1ab5ba1..33201653 100644 --- a/static/ts/components/date-time-input.ts +++ b/static/ts/components/date-time-input.ts @@ -27,7 +27,9 @@ import { makeElement, parseDate } from '../lib' */ class DateTimeInput extends /* HTMLInputElement */ HTMLElement { + /** Our time input element */ readonly time: HTMLInputElement; + /** Our date input element */ readonly date: HTMLInputElement; constructor() { @@ -43,22 +45,30 @@ class DateTimeInput extends /* HTMLInputElement */ HTMLElement { }) as HTMLInputElement } + /** + We set our children first when mounted. + + This can be in the constructor for chromium, but NOT firefox... + + - Vivaldi 4.3.2439.63 stable + - Mozilla Firefox 94.0.1 + */ 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) } + /** + Attributes which we want notifications when they are change. + + Part of the Web Component API + + - `dateonly` + */ static get observedAttributes() { return ['dateonly'] } + /** Part of the Web Component API */ attributeChangedCallback(name: string, _: string | null, to: string | null): void { switch (name) { case 'dateonly': @@ -84,6 +94,7 @@ class DateTimeInput extends /* HTMLInputElement */ HTMLElement { return this.hasAttribute('dateonly'); } + /** See getter */ set dateonly(b: boolean) { if (b) { this.setAttribute('dateonly', ""); @@ -92,6 +103,7 @@ class DateTimeInput extends /* HTMLInputElement */ HTMLElement { } } + /** See getter */ set value(date: Date) { let [d, t] = date.format("~L~Y-~m-~dT~H:~M").split('T'); this.date.value = d; @@ -124,6 +136,13 @@ class DateTimeInput extends /* HTMLInputElement */ HTMLElement { } } + /** + Set the selected date. + + @param new_value + If given a date, set the input to that date. + If given a string, parse it as an ISO-8601 formatted datetime. + */ set stringValue(new_value: Date | string) { let date, time, dateonly = false; if (new_value instanceof Date) { @@ -138,6 +157,9 @@ class DateTimeInput extends /* HTMLInputElement */ HTMLElement { this.time.value = time; } + /** + Adds an event listener to both the date and time input. + */ addEventListener(type: string, proc: ((e: Event) => void)) { if (type != 'input') throw "Only input supported"; diff --git a/static/ts/components/input-list.ts b/static/ts/components/input-list.ts index 31dd5158..72d27cab 100644 --- a/static/ts/components/input-list.ts +++ b/static/ts/components/input-list.ts @@ -14,26 +14,62 @@ export { InputList } TODO allow each item to be a larger unit, possibly containing multiple input fields. */ +/** + A multi-valued input, done by creating extra input fields as needed. + + The first element of body MUST be an input element, which will be used as the + template for each instance. A tag input could for example look like + + @example + ```html + + + + ``` + + Whenever one of the input elements `value` becomes the empty string, that tag + is removed, and whenever there is no element with the empty string as a + `value`, a new input element will be added onto the end. + */ class InputList extends HTMLElement { - el: HTMLInputElement; + /** The element used as our template. Will be sourced from the initial HTML code. */ + #el: HTMLInputElement; + /** + Registered listeners, which will be added onto each created entry + + Keys are event names ('input', 'change', ...) and values event handlers. + + This is a list of tuples rather than a dictionary, since multiple + listeners of the same type can be registered. + */ #listeners: [string, (e: Event) => void][] = []; constructor() { super(); - this.el = this.children[0].cloneNode(true) as HTMLInputElement; + this.#el = this.children[0].cloneNode(true) as HTMLInputElement; } + /** Clears all existing children upon mount */ connectedCallback() { for (let child of this.children) { child.remove(); } - this.addInstance(); + this.#addInstance(); } - createInstance(): HTMLInputElement { - let new_el = this.el.cloneNode(true) as HTMLInputElement + /** + Instanciates a new instance of the input element. + + An event listener for 'input' will be added, which will handle the + addition and removing of other elements. + + All event listeners attachet on the input-list component will also be + added. + */ + #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 @@ -49,7 +85,7 @@ class InputList extends HTMLElement { } } else { if (!this.nextElementSibling) { - that.addInstance(); + that.#addInstance(); // window.setTimeout(() => this.focus()) this.focus(); } @@ -63,8 +99,9 @@ class InputList extends HTMLElement { return new_el; } - addInstance() { - let new_el = this.createInstance(); + /** Add a new instance of the input element to the container */ + #addInstance() { + let new_el = this.#createInstance(); this.appendChild(new_el); } @@ -72,7 +109,7 @@ class InputList extends HTMLElement { * The value from each element, except the last which should always be empty. * Has an unspecified type, since children:s value field might give non-strings. */ - get value(): any[] { + get value(): unknown[] { let value_list = [] for (let child of this.children) { value_list.push((child as any).value); @@ -83,6 +120,12 @@ class InputList extends HTMLElement { return value_list } + /** + Overwrite the current value with a new one. + + Each entry in the array will be mapped unto one instance of the template + input element. A final empty element will also be added. + */ set value(new_value: any[]) { let all_equal = true; @@ -111,17 +154,22 @@ class InputList extends HTMLElement { /* clear dictionary */ values.set(value, false); } else { - let new_el = this.createInstance(); + let new_el = this.#createInstance(); new_el.value = value; output_list.push(new_el); } } /* final, trailing, element */ - output_list.push(this.createInstance()); + output_list.push(this.#createInstance()); this.replaceChildren(...output_list); } + /** + Add an event listener to each of the inputs. + + This basically works as the "regular" version. + */ addEventListener(type: string, proc: ((e: Event) => void)) { // if (type != 'input') throw "Only input supported"; diff --git a/static/ts/components/popup-element.ts b/static/ts/components/popup-element.ts index cc011ce3..a1e81f0e 100644 --- a/static/ts/components/popup-element.ts +++ b/static/ts/components/popup-element.ts @@ -90,6 +90,7 @@ class PopupElement extends ComponentVEvent { this.replaceChildren(body); } + /** ['visible'] */ static get observedAttributes() { return ['visible']; } @@ -99,7 +100,7 @@ class PopupElement extends ComponentVEvent { case 'visible': if (newValue !== null) /* Only run resize code when showing the popup */ - this.onVisibilityChange() + this.#onVisibilityChange() break; } } @@ -114,6 +115,7 @@ class PopupElement extends ComponentVEvent { return this.hasAttribute('visible'); } + /** Set the visibility status of the component. */ set visible(isVisible: boolean) { if (isVisible) { this.setAttribute('visible', 'visible'); @@ -122,7 +124,7 @@ class PopupElement extends ComponentVEvent { } } - private onVisibilityChange() { + #onVisibilityChange() { console.log('here'); /* TODO better way to find root */ diff --git a/static/ts/components/slider.ts b/static/ts/components/slider.ts index 48abc91b..8be66a73 100644 --- a/static/ts/components/slider.ts +++ b/static/ts/components/slider.ts @@ -1,24 +1,58 @@ -export { SliderInput } +/** + + + A Web Component implementing a slider with a corresponding number input. + + TODO rename this file + + ### Parameters + + All of these are optional, see {@linkcode dflt} for defaults. + + #### min + Minimum allowed value. + + #### max + Maximum allowed value. + + #### step + How large each step of the slider/number box should be. + + @module +*/ + +export { SliderInput, Attribute, dflt } import { makeElement } from '../lib' +/** Defalut values for all attributes, if not given */ const dflt = { min: 0, max: 100, step: 1, } +/** Valid attributes for SliderInput */ type Attribute = 'min' | 'max' | 'step' +/** + Component displaying an input slider, together with a corresponding numerical + input +*/ class SliderInput extends HTMLElement { /* value a string since javascript kind of expects that */ - #value = "0"; - min = 0; - max = 100; - step = 1; - + #value = "" + dflt.min + /** Minimum allowed value */ + min = dflt.min + /** Maximum allowed value */ + max = dflt.max + /** How large each step should be */ + step = dflt.step + + /** The HTML slider component */ readonly slider: HTMLInputElement; + /** The HTML number input component */ readonly textIn: HTMLInputElement; constructor(min?: number, max?: number, step?: number, value?: number) { @@ -48,8 +82,8 @@ class SliderInput extends HTMLElement { value: this.value, }) as HTMLInputElement - this.slider.addEventListener('input', e => this.propagate(e)); - this.textIn.addEventListener('input', e => this.propagate(e)); + 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); @@ -59,7 +93,7 @@ class SliderInput extends HTMLElement { this.replaceChildren(this.slider, this.textIn); } - + /** ['min', 'max', 'step'] */ static get observedAttributes(): Attribute[] { return ['min', 'max', 'step'] } @@ -75,19 +109,35 @@ class SliderInput extends HTMLElement { this[name] = parseFloat(to || "" + dflt[name]) } - propagate(e: Event) { + /** + Helper for updating the value attribute + + Event listeners are bound on both the input elements, which both simply + call this. This procedure then updates the classes value field. + + TODO `oninput`? + */ + #propagate(e: Event) { this.value = (e.target as HTMLInputElement).value; if (e instanceof InputEvent && this.oninput) { this.oninput(e); } } + /** + Set a new numerical value. + + A number not possible due to the current `min`, `max`, and `step` + properties can be set and will work, the slider will however not + properly show it, but rather the closest value it can display. + */ set value(value: string) { this.slider.value = value; this.textIn.value = value; this.#value = value; } + /** Get the current numerical value */ get value(): string { return this.#value; } diff --git a/static/ts/components/tab-group-element.ts b/static/ts/components/tab-group-element.ts index ce532cec..bcd45b40 100644 --- a/static/ts/components/tab-group-element.ts +++ b/static/ts/components/tab-group-element.ts @@ -58,9 +58,12 @@ export { TabGroupElement } */ class TabGroupElement extends ComponentVEvent { + /** The container holding all the tabLabels */ readonly menu: HTMLElement; + /** Contents of each tab */ tabs: HTMLElement[] = []; + /** Label element of each tab */ tabLabels: HTMLElement[] = []; constructor(uid?: string) { @@ -166,7 +169,7 @@ class TabGroupElement extends ComponentVEvent { this.tabLabels.push(tabLabel); this.menu.appendChild(tabLabel); - tabLabel.addEventListener('click', () => this.tabClickedCallback(tabLabel)); + tabLabel.addEventListener('click', () => this.#tabClickedCallback(tabLabel)); this.style.setProperty('--tabcount', '' + this.tabs.length); } @@ -199,7 +202,7 @@ class TabGroupElement extends ComponentVEvent { } /* TODO replace querySelectors here with our already saved references */ - tabClickedCallback(tab: Element) { + #tabClickedCallback(tab: Element) { /* hide all tab panels */ for (let tabcontent of this.querySelectorAll('[role="tabpanel"]')) { @@ -218,7 +221,7 @@ class TabGroupElement extends ComponentVEvent { } - /* returns our rrule tab if we have one */ + /** Return our rrule tab if we have one */ has_rrule_tab(): Element | false { for (let child of this.children) { if (child.firstChild! instanceof EditRRule) { diff --git a/static/ts/components/vevent-block.ts b/static/ts/components/vevent-block.ts index 374cf103..90460740 100644 --- a/static/ts/components/vevent-block.ts +++ b/static/ts/components/vevent-block.ts @@ -16,10 +16,16 @@ import { VEvent } from '../vevent' import { parseDate, to_local } from '../lib' -/* +/** + A graphical block in the inline view. - A grahpical block in the week view. -*/ + The back-end links what should become these to elements in the sidebar + containing extra info, jumping between them using fragment links. + That functionality is removed when we replace the non-js fallback children of + these elements, but we instead link it to a + {@linkcode components/popup-element.PopupElement} + containing the detailed information, along with editing controls and more. + */ class ComponentBlock extends ComponentVEvent { constructor(uid?: string) { super(uid); diff --git a/static/ts/components/vevent.ts b/static/ts/components/vevent.ts index 1d400e1e..50ff4a30 100644 --- a/static/ts/components/vevent.ts +++ b/static/ts/components/vevent.ts @@ -16,9 +16,23 @@ export { ComponentVEvent } import { vcal_objects } from '../globals' import { VEvent } from '../vevent' +/** + Base class for all Web Components closely linked with VEvents. + + TODO document how templates work. + + TODO document lifecycle, and how objects are fetched from the "global" store. + */ abstract class ComponentVEvent extends HTMLElement { + /** + The template for this event. + + TODO document how this is populate + */ template?: HTMLTemplateElement + + /** The UID of the VEvent we are tracking */ uid: string /** @@ -72,6 +86,11 @@ abstract class ComponentVEvent extends HTMLElement { should take care of that some other way */ } + /** + Called when the component is mounted. + + Redraws the target if the wanted object is available at that time. + */ connectedCallback() { let uid = this.dataset.uid if (uid) { -- cgit v1.2.3