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/Makefile | 12 +- static/README.md | 10 ++ static/package.json | 1 + static/ts/clock.ts | 54 ++++++-- static/ts/components.ts | 13 ++ 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 +++ static/ts/event-creator.ts | 77 ++++++++--- static/ts/formatters.ts | 6 +- static/ts/globals.ts | 4 + static/ts/jcal.ts | 30 +++++ static/ts/lib.ts | 216 ++++++++++++++++++------------ static/ts/types.ts | 42 ++++-- static/ts/vevent.ts | 126 +++++++++++++---- static/tsconfig.json | 16 ++- 22 files changed, 680 insertions(+), 229 deletions(-) diff --git a/static/Makefile b/static/Makefile index 17432585..00401503 100644 --- a/static/Makefile +++ b/static/Makefile @@ -15,7 +15,7 @@ __ESBUILD_FLAGS = --log-level=$(ESBUILD_LOGLEVEL) \ --sourcemap --bundle --outdir=$(CURDIR)/out \ $(ESBUILD_FLAGS) -export PATH := $(shell npm bin):$(PATH) +export PATH := $(CURDIR)/node_modules/.bin/:$(PATH) all: $(TARGETS) @@ -42,5 +42,13 @@ clean: out/%.css: scss/%.scss scss -E UTF-8 $(WATCH) -I. $< $@ +# The grep expression is to supress irrelevant warning messages. +# - __type since the extensions to base classes propagate to many +# events, but typedoc only documents them if in the entry point +# - [.]# since it's sometimes sensible to not document +# implementation details. +# - connectedCallback, attributeChangedCallabck: These are part +# of the standard API for Web Components, and usually have nothing +# interesting to note. doc: - typedoc --logLevel Verbose --excludeExternals + typedoc --excludeExternals |& grep -vE '(__type|[.]#|connectedCallback|attributeChangedCallback)' diff --git a/static/README.md b/static/README.md index fe5f775c..1cb18411 100644 --- a/static/README.md +++ b/static/README.md @@ -1,5 +1,15 @@ The frontend code has its entry-point in `script.ts`. +Much of this code assumes prior knowledge of the iCalendar standard (RFC +5545). Besides that, the term "VComponent" is used to refer to any Calendar +Component as specified in that RFC under 3.6. + +## Data Flow + +TODO document how data gets from the server to us, and from us to the server. + +A large part of this is how much we convert between serialization formats. + ## web components All elements are initialized in components.ts diff --git a/static/package.json b/static/package.json index f376031d..f671def1 100644 --- a/static/package.json +++ b/static/package.json @@ -7,6 +7,7 @@ "@types/uuid": "^8.3.1" }, "optionalDependencies": { + "comment-json": "^4.2.3", "madge": "^5.0.1" }, "dependencies": { diff --git a/static/ts/clock.ts b/static/ts/clock.ts index a0e4670a..11b2b2c5 100644 --- a/static/ts/clock.ts +++ b/static/ts/clock.ts @@ -10,6 +10,8 @@ * * TODO shouldn't these be defined with the rest of the components? * + * TODO why isn't Timebar and SmallCellHighlight also Web Components? + * * @module */ @@ -35,6 +37,8 @@ class Timebar extends Clock { // start_time: Date // end_time: Date + + /** The bar to update */ bar_object: HTMLElement | null constructor(/*start_time: Date, end_time: Date*/) { @@ -74,7 +78,12 @@ class Timebar extends Clock { */ class SmallcalCellHighlight extends Clock { + /** The calendar which a cell should be highlighted in */ small_cal: HTMLElement + /** + The currently highlighted cell, or `null` if no cell should be + should be highlighted (such as if a non-current month is selected + */ current_cell: HTMLElement | null /** @@ -105,8 +114,16 @@ class SmallcalCellHighlight extends Clock { /* -------------------------------------------------- */ +/** + Base class for custom HTML elements which wants to be updated for a human + timescale. + + When creating, the attribute `interval` can be given, which specifies (in + seconds) how often the component should be updated. +*/ class ClockElement extends HTMLElement { + /** Javascript timer id. Used if the timer needs to be canceled */ timer_id: number constructor() { @@ -125,21 +142,24 @@ class ClockElement extends HTMLElement { this.update(new Date) } - static get observedAttributes() { - return ['timer_id'] - } - + /** + Method which is called each "tick" (see interval) + @param date + The current timestamp when the function is called. + */ update(_: Date) { /* noop */ } } /** - * Updates the ``Today'' link in the side panel to point directly to the - * correct web-address. The link works without JavaScript, but then - * requires a redirect from the server. - * - * All actual updating logic is already abstracted away. It would be - * desirable if something more was done with this. + A "button" which always points to the link "~Y-~m-~d.html". + + This class is bound to the web component + + In the backend code, a `/today` endpoint exists. That however requires that + we ask the server for the correct URL, and follow a 300 (series) redirect. + + Since the URL:s are stable, it's possible to jump directly to the given page. */ class TodayButton extends ClockElement { a: HTMLAnchorElement; @@ -162,12 +182,26 @@ class TodayButton extends ClockElement { } +/** + A component which displays the current time + + This class is bound to the web component + + It currently is hard-coded to display time on the format ~H:~M:~S. +*/ class CurrentTime extends ClockElement { update(now: Date) { this.textContent = now.format('~H:~M:~S') } } +/** + Create Web Components mentioned on this page. + + MUST be called early on in the execution. + + TODO this should be merged with other web component declarations. +*/ function initialize_clock_components() { customElements.define('today-button', TodayButton) customElements.define('current-time', CurrentTime) diff --git a/static/ts/components.ts b/static/ts/components.ts index cbd6dc9b..c78b5753 100644 --- a/static/ts/components.ts +++ b/static/ts/components.ts @@ -22,6 +22,19 @@ import { DateJump } from './components/date-jump' export { initialize_components } +/** + Create web components from all our components. + + The reason each components module doesn't simply initialize its own component + is due to some components needing to be initialized AFTER some global + variables (see inline comments). + + @TODO + Fix the initialization order dependency + + @TODO + or otherwise have a static field on each component specifying it's desired name. + */ function initialize_components() { /* These MUST be created AFTER vcal_objcets and event_calendar_mapping are inistialized, since their constructors assume that that piece of global 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) { diff --git a/static/ts/event-creator.ts b/static/ts/event-creator.ts index 5e55e64e..a3231d24 100644 --- a/static/ts/event-creator.ts +++ b/static/ts/event-creator.ts @@ -6,40 +6,69 @@ import { ComponentBlock } from './components/vevent-block' import { round_time, parseDate } from './lib' import { ical_type } from './types' +/** + Class managing the state while creating events. + + This is mainly for, when in the UI, the user starts to create events by + dragging on the calendar. + +*/ class EventCreator { - /* Event which we are trying to create */ + /** Event which we are trying to create */ ev?: VEvent - /* Graphical block for event. Only here so we can find its siblings, - and update pointer events accordingly */ + /** Graphical block for event. Only here so we can find its siblings, and + update pointer events accordingly */ event?: Element - event_start: { x: number, y: number } = { x: NaN, y: NaN } - down_on_event: boolean = false - timeStart: number = 0 + /** + Where the mouse down for the event started. + + This is here to check if the user is actually dragging, or just randomly + clicking on the background with a shaky hand. + + There are some constants in the code for what a shaky hand means + (currently less than 10 pixels in X, or 5 in Y) + */ + #event_start: { x: number, y: number } = { x: NaN, y: NaN } + #down_on_event: boolean = false + #time_start: number = 0 + /** + Event handler for `mosedown` events. + */ create_event_down(intended_target: HTMLElement): (e: MouseEvent) => any { let that = this; return function(e: MouseEvent) { /* Only trigger event creation stuff on actuall events background, NOT on its children */ - that.down_on_event = false; + that.#down_on_event = false; if (e.target != intended_target) return; - that.down_on_event = true; + that.#down_on_event = true; - that.event_start.x = e.clientX; - that.event_start.y = e.clientY; + that.#event_start.x = e.clientX; + that.#event_start.y = e.clientY; } } - /* - round_to: what start and end times should round to when dragging, in fractionsb - of the width of the containing container. + /** + Event handler for `mousemove` events. - TODO limit this to only continue when on the intended event_container. + @param pos_in + TODO - (event → [0, 1)), 𝐑, bool → event → () + @param round_to + what start and end times should round to when dragging, in fractionsb of + the width of the containing container. + + @param wide_element + Does the element expect to grow horizontally (`true`) or vertically + (`false`). + + TODO limit this to only continue when on the intended event_container. + + (event → [0, 1)), 𝐑, bool → event → () */ create_event_move( pos_in: ((c: HTMLElement, e: MouseEvent) => number), @@ -48,13 +77,13 @@ class EventCreator { ): ((e: MouseEvent) => void) { let that = this; return function(this: HTMLElement, e: MouseEvent) { - if (e.buttons != 1 || !that.down_on_event) return; + if (e.buttons != 1 || !that.#down_on_event) return; /* Create event when we start moving the mouse. */ if (!that.ev) { /* Small deadzone so tiny click and drags aren't registered */ - if (Math.abs(that.event_start.x - e.clientX) < 10 - && Math.abs(that.event_start.y - e.clientY) < 5) { return; } + if (Math.abs(that.#event_start.x - e.clientX) < 10 + && Math.abs(that.#event_start.y - e.clientY) < 5) { return; } /* only allow start of dragging on background */ if (e.target !== this) return; @@ -112,7 +141,7 @@ class EventCreator { (e as HTMLElement).style.pointerEvents = "none"; } - that.timeStart = round_time(pos_in(this, e), round_to); + that.#time_start = round_time(pos_in(this, e), round_to); } let time = round_time(pos_in(this, e), round_to); @@ -136,8 +165,8 @@ class EventCreator { /* ms */ let duration = container_end.valueOf() - container_start.valueOf(); - let start_in_duration = duration * Math.min(that.timeStart, time); - let end_in_duration = duration * Math.max(that.timeStart, time); + let start_in_duration = duration * Math.min(that.#time_start, time); + let end_in_duration = duration * Math.max(that.#time_start, time); /* Notice that these are converted to UTC, since the intervals are given in utc, and I only really care about local time (which a specific local @@ -158,6 +187,12 @@ class EventCreator { } } + /** + Event handler for `mouseup` events. + + TODO callback? + TODO return value? + */ create_event_finisher(callback: ((ev: VEvent) => void)) { let that = this; return function create_event_up(_: MouseEvent) { diff --git a/static/ts/formatters.ts b/static/ts/formatters.ts index 05f84d31..b5c55913 100644 --- a/static/ts/formatters.ts +++ b/static/ts/formatters.ts @@ -1,13 +1,13 @@ /** * Formatting procedures used by some components. * - * // TODO can we have a backref of every node containing {@link formatters-proc}? + * // TODO can we have a backref of every node containing `{@link formatters-proc}`? * - * {@label formatters} + * {@label FORMATTERS} * * Each procedure takes three arguments. The HTML-element which contents * should be replaced, the VEvent containing all data, and the target - * value, as returned by {@link VEvent.getProperty}. + * value, as returned by {@linkcode vevent.VEvent.getProperty}. * * Also bound to the window object. * diff --git a/static/ts/globals.ts b/static/ts/globals.ts index 75fb1df9..1cdf1733 100644 --- a/static/ts/globals.ts +++ b/static/ts/globals.ts @@ -76,6 +76,10 @@ window.addNewEvent = () => { popup.maximize(); } +/** + Find the calendar block in the inline view containing the VEvent identified + by the uid +*/ function find_block(uid: uid): ComponentBlock | null { let obj = vcal_objects.get(uid) if (obj === undefined) { diff --git a/static/ts/jcal.ts b/static/ts/jcal.ts index 6a491e04..feac297b 100644 --- a/static/ts/jcal.ts +++ b/static/ts/jcal.ts @@ -1,3 +1,10 @@ +/** + Operations for working with jCal. + + jCal is defined in RFC 7265, and is a JSON mapping of the iCalendar standard. +*/ + + export { jcal_to_xcal } import { xcal, ical_type, JCalProperty, JCal } from './types' @@ -161,6 +168,17 @@ function jcal_property_to_xcal_property( } +/** + Convert a jCal document into an xCal document. + + @param jcals A list of jcal components. Most iCal formats supports multiple + "root" levels components. jCal might do it, which is why this parameter is + multi-valued. + + @return A document note which is the root of an xCal document. + The root will be an icalendar tag, with each child getting its data from each + element of the input. + */ function jcal_to_xcal(...jcals: JCal[]): Document { let doc = document.implementation.createDocument(xcal, 'icalendar'); for (let jcal of jcals) { @@ -169,6 +187,18 @@ function jcal_to_xcal(...jcals: JCal[]): Document { return doc; } +/** + Convert a single jCal entry into a single xCal entry. + + @param doc + A Document element in the xcal namespace. + + @param jcal + The object to convert + + @return + A 1-to-1 mapping of the jCal object as xCal. + */ function jcal_to_xcal_inner(doc: Document, jcal: JCal) { let [tagname, properties, components] = jcal; diff --git a/static/ts/lib.ts b/static/ts/lib.ts index 14cdd4f2..d503ac5d 100644 --- a/static/ts/lib.ts +++ b/static/ts/lib.ts @@ -1,90 +1,16 @@ /** - * General procedures which in theory could be used anywhere. - * - * Besides exporting the mentioned functions, this module also extends some base - * classes. - * - * ```tex -@node Default prototype extensions -@subsubsection Default prototype extensions - - - -@defmethod HTMLElement addEventListener name proc -Replace the default @code{addEventListener} with a version that stores -all listeners in the dictionary @var{listeners}. -@end defmethod - -@defivar HTMLElement listeners -Dictionary of all registered listeners to this element. -Keys are taken from @code{addEventListener}. -@end defivar - -@defmethod DOMTokenList find regexp -Finds the first element of the DOMTokenList whichs value matches -the supplied regexp. Returns a pair of the index and the value. -@end defmethod - -@defmethod Object format args ... -Returns a string representation of the given object. -Allows extending for custom types, -@ref{date-format} -@end defmethod - * ``` - * - * --- - * - * ```tex -Some extensions to the builtin class ``Date'' is made. - -@defivar Date utc -Boolean indicating if the given timestamp is in UTC or local time. -true means UTC. -@end defivar + General procedures which in theory could be used anywhere. -@defivar Date dateonly -Boolean indicating if the time component of the Date object should be disregarded. -@end defivar + Besides exporting the mentioned functions, this module also + extends some base classes. - * ``` - * ```tex -@defmethod Date format str args ... -@anchor{date-format} -Formats a Date object according to the format specification @var{str}. -Keeping with Guile each format specifier starts with a ~. - -@table @samp -@item ~~ -literal ~ -@c Almost all fields are left padded. How do I signify this -@c with a single footnote? -@item ~Y -year, left-padding with zeroes. -@item ~m -month number, left padded with zeroes. -@item ~d -day of month. -@item ~H -hour -@item ~M -minute -@item ~S -second -@item ~Z -'Z' if Date is UTC, otherwise nothing -@item ~L -Converts the date to local time -(@pxref{to_local}) (doesn't modify source object). Outputs nothing -@end table -@end defmethod -``` - * - * @module + @module */ export { makeElement, date_to_percent, parseDate, gensym, to_local, to_boolean, - asList, round_time + asList, round_time, + format_date, } /* @@ -92,37 +18,95 @@ export { */ declare global { interface Object { + /** + Introduce a format method on ALL objects + + The default implementation simply stringifies the object, but this + allows any component to overwrite it, allowing for generic custom + formatting of data. + + This also means that the format string is ignored for the default + implementation. + + See `Data.prototype.format`. + */ format: (fmt: string) => string } /** HTMLElement extensions */ interface HTMLElement { + /** + "Private" property, storing the "true" add event listener. The + exposed addEventListener is later overwritten to also store a list of + which event listeners are added. + */ _addEventListener: (name: string, proc: (e: Event) => void) => void - listeners: Map void)[]> + + /** + Contains all listeners added through `addEventListener`. + + The keys are the same as to `addEventListener` ('load', 'mouseover', + ...) + + Values are simply a list of all added listeners, probably in addition + order. + */ + listeners: Map void)[]>; + + /** + Returns listeners. + + TODO why does this exist? + */ getListeners: () => Map void)[]> } interface Date { + /** + A proper date formatter for javascript. + + See {@ref format_date} for details + */ format: (fmt: string) => string + + /** Indicates if the date object is in UTC or local time. */ utc: boolean + + /** + Indicates that the object only contains a date component. + + This means that any time is ignored in most operations. + */ dateonly: boolean // type: 'date' | 'date-time' } interface DOMTokenList { + /** + Searches a DOMTokenList for anything matching. + + DOMTokenLists are returned by `.classList` and similar. + + @return Returns the matching index, and the matched value, + or `undefined` if nothing was found. + */ find: (regex: string) => [number, string] | undefined } interface HTMLCollection { + /** Adds an iterator to HTMLCollections */ forEach: (proc: ((el: Element) => void)) => void } interface HTMLCollectionOf { + /** Adds an iterator to HTMLCollections */ forEach: (proc: ((el: T) => void)) => void } } +/** See interface above */ HTMLElement.prototype._addEventListener = HTMLElement.prototype.addEventListener; +/** See interface above */ HTMLElement.prototype.addEventListener = function(name: string, proc: (e: Event) => void) { if (!this.listeners) this.listeners = new Map if (!this.listeners.get(name)) this.listeners.set(name, []); @@ -130,6 +114,7 @@ HTMLElement.prototype.addEventListener = function(name: string, proc: (e: Event) this.listeners.get(name)!.push(proc); return this._addEventListener(name, proc); }; +/** See interface above */ HTMLElement.prototype.getListeners = function() { return this.listeners; } @@ -199,7 +184,6 @@ function parseDate(str: string): Date { return date; } -/* @anchor{to_local} */ /** * Returns a Date object (which may be new) which is guaranteed in local time. * This means that the `utc` field is `false`, and that @@ -245,7 +229,21 @@ function makeElement(name: string, attr = {}, actualAttr = {}): HTMLElement { return element; } -/** TODO document me */ +/** + Round clock time to closest interval. + + @param time + The desired clock-time, in decimal time. So 12:30 would be given as 12.30. + + @param fraction + The time interval to round to. To round to nearest half hour, give 0.5. + + @example + ```js + > round_time(10.1, 15/60) + 10 + ``` + */ function round_time(time: number, fraction: number): number { let scale = 1 / fraction; return Math.round(time * scale) / scale; @@ -286,6 +284,16 @@ function asList(thing: Array | T): Array { } +/** + Smartly converts a value into a boolean. + + Booleans are returned as if, + + Strings are parsed, mapping `'true'` onto `true`, `'false'` onto `false`, + empty strings onto `false`, and anything else onto `true`. + + Anything else is left onto JavaScript to coerce a boolean. + */ function to_boolean(value: any): boolean { switch (typeof value) { case 'string': @@ -309,13 +317,43 @@ function datepad(thing: number | string, width = 2): string { return (thing + "").padStart(width, "0"); } -/** Equivalent to `date.format(str)` */ -function format_date(date: Date, str: string): string { +/** + Format a date into a string. + + @param date + The datetime to format + + @param format + How the date should be converted into a string. + + The format is similar to `strftime`, but with tilde (`~`) characters + instead of percent signs, to match how Scheme does it. Valid format + specifiers are: + + | Fmt | Output | Width¹ | + |------|----------------------------------|--------| + | `~~` | Literal '~' | | + | `~Y` | Year | 4 | + | `~m` | Month number | 2 | + | `~d` | Day of month | 2 | + | `~H` | Hour | 2 | + | `~M` | Minute | 2 | + | `~S` | Second | 2 | + | `~Z` | 'Z' if date is UTC, otherwise '' | | + | `~L` | Converts date to local time² | | + + - ¹ These fields will be left padded with zeroes to that width + - ² This forces the output to be in local time, possibly converting + timezone if needed. It then outputs nothing. + See {@link to_local `to_local`} for details. + +*/ +function format_date(date: Date, format: string): string { let fmtmode = false; let outstr = ""; - for (var i = 0; i < str.length; i++) { + for (var i = 0; i < format.length; i++) { if (fmtmode) { - switch (str[i]) { + switch (format[i]) { /* Moves the date into local time. */ case 'L': date = to_local(date); break; case 'Y': outstr += datepad(date.getFullYear(), 4); break; @@ -327,10 +365,10 @@ function format_date(date: Date, str: string): string { case 'Z': if (date.utc) outstr += 'Z'; break; } fmtmode = false; - } else if (str[i] == '~') { + } else if (format[i] == '~') { fmtmode = true; } else { - outstr += str[i]; + outstr += format[i]; } } return outstr; diff --git a/static/ts/types.ts b/static/ts/types.ts index a0ab74a4..a01f6672 100644 --- a/static/ts/types.ts +++ b/static/ts/types.ts @@ -30,7 +30,7 @@ let all_types = [ ] -/* The union of all elements in `all_types`. */ +/** The union of all elements in `all_types`. */ type ical_type = 'text' | 'uri' @@ -104,6 +104,12 @@ let valid_fields: Map = new Map([ valid_fields.set('DAYLIGHT', valid_fields.get('STANDARD')!); +/** + All registered property types for VComponents. + + Note that only some of these are valid for each type of component (VCALENDAR, + VEVENT, ...), and that they all support both iana and `x-` extensions. + */ type known_ical_types = 'ACTION' | 'ATTACH' @@ -217,10 +223,7 @@ let valid_input_types: Map> = // type JCalLine { // } -/** Alias of (`'vevent' | string`). */ -type tagname = 'vevent' | string - -/** Alias of `string`. */ +/** The UID of a VEvent, to make declarations clearer. */ type uid = string /* TODO is this type correct? @@ -240,17 +243,40 @@ type JCalProperty | [string, Record, ical_type, ...any[]] /** - * A record consisting of a `tagname`, a list of - * `JCalProperties`, and a list of other `JCal` objects. + Base type for JCal documents. + + Each VComponent in a JCal document is of this form. + + - The first element is the components type + ('vevent', 'vcalendar', ...), in all lower case + - The second element is is all properties directly + on the component. + - The third element is a list of all children. */ -type JCal = [tagname, JCalProperty[], JCal[]] +type JCal = [string, JCalProperty[], JCal[]] /** The xml namespace name for xcalendar */ const xcal = "urn:ietf:params:xml:ns:icalendar-2.0"; +/** + An entry into a changelog. + + This is primarily used by VEvent, to track what has happened during a + session. + */ interface ChangeLogEntry { + /** + Type of change + + 'property' is used for changes to properties. + + 'calendar' is used when the containing calendar of a VEVENT is changed + */ type: 'calendar' | 'property', + /** The name of the filed changed */ name: string, + /** The previous value, `null` if just created */ from: string | null, + /** The new value, `null` if removed */ to: string | null, } diff --git a/static/ts/vevent.ts b/static/ts/vevent.ts index 07f25d02..6aaa6984 100644 --- a/static/ts/vevent.ts +++ b/static/ts/vevent.ts @@ -2,13 +2,20 @@ import { ical_type, valid_input_types, JCal, JCalProperty, ChangeLogEntry } from import { parseDate } from './lib' export { - VEvent, xml_to_vcal, RecurrenceRule, + Redrawable, + VEvent, + VEventValue, + freqType, isRedrawable, + list_values, + weekday, + xml_to_vcal, } /** Something which can be redrawn */ interface Redrawable extends HTMLElement { + /** Method which will be called upon a redraw request. */ redraw(data: VEvent): void } @@ -17,14 +24,27 @@ function isRedrawable(x: HTMLElement): x is Redrawable { return 'redraw' in x } +/** + A single value from a vcomponent. + This is basically a type tagged tuple, with an optional map of parameters. +*/ class VEventValue { + /** The value type of the contained value. */ type: ical_type - /* value should NEVER be a list, since multi-valued properties should - be split into multiple VEventValue objects! */ + /** + The actual value. + + Should NEVER be a list, since those are coded as + lists of `VEventValue`:s in `Vevent.properties` + */ value: any + + /** + VComponent parameters attached to the value. + */ parameters: Map constructor(type: ical_type, value: any, parameters = new Map) { @@ -82,34 +102,46 @@ class VEventValue { } /* TODO maybe ... */ -class VEventDuration extends VEventValue { -} +// class VEventDuration extends VEventValue { +// } +/** VComponent properties which contain lists */ type list_values = 'categories' | 'resources' | 'freebusy' | 'exdate' | 'rdate' | 'CATEGORIES' | 'RESOURCES' | 'FREEBUSY' | 'EXDATE' | 'RDATE'; -/* - Abstract representation of a calendar event (or similar). -All "live" calendar data in the frontend should live in an object of this type. - */ /** - * Component for a single instance of a calendar event. Almost all data - * access should go through `getProperty` and `setProperty`, - * with the exception of the current calendar (which is accessed directly - * through `calendar`). Almost all changes through these interfaces - * are logged, and can be viewed through `changelog`. + This class is the data container for the underlying VEVENT objects in the + backend calendar files. They also keep track on all Web Components which + wants to render part of the event. + + Note that despite the name this component isn't limited to VEVENT:s, but is + used for all VComponents in the tree. This means that even calendars and + alarms can be instances of this class. + + Property access is done through `getProperty` and `setProperty` (properties + are things such as 'SUMMARY', 'DTSTART', ...) */ class VEvent { - /* Calendar properties */ + /** + Properties bound directly on this object. + + These are things such as 'DTSTART', 'SUMMARY', ... + */ private properties: Map - /* Children (such as alarms for events) */ + /** + Children to this component. + + Valid children depends on the type. For example, for calendars this is + primarily events, while for events it's alarm components + */ components: VEvent[] - /* HTMLElements which wants to be redrawn when this object changes. - Elements can be registered with the @code{register} method. + /** + HTMLElements which wants to be redrawn when this object changes. + Elements can be registered with the `register` method. */ registered: Redrawable[] @@ -122,13 +154,19 @@ class VEvent { */ #changelog: ChangeLogEntry[] = [] - /* Iterator instead of direct return to ensure the receiver doesn't - modify the array */ - /** Public (read only) interface to changelog. */ + /** + The changelog for this component. + + An iterator is returned rather than an array, to ensure modifications are + impossible. + */ get changelog(): IterableIterator<[number, ChangeLogEntry]> { return this.#changelog.entries(); } + /** + Add an entry to the changelog. + */ addlog(entry: ChangeLogEntry) { let len = this.#changelog.length let last = this.#changelog[len - 1] @@ -153,6 +191,17 @@ class VEvent { } } + /** + Construct a new Component. + + @param properties + Initial properties for the component + + @param components + Initial children for the component + + TODO where is the type of the component registered? + */ constructor( properties: Map = new Map(), components: VEvent[] = [] @@ -202,7 +251,7 @@ class VEvent { return this.properties.keys() } - private setPropertyInternal(key: string, value: any, type?: ical_type) { + #setPropertyInternal(key: string, value: any, type?: ical_type) { function resolve_type(key: string, type?: ical_type): ical_type { if (type) { return type; @@ -273,7 +322,7 @@ class VEvent { * objects, notifying them of the change. */ setProperty(key: string, value: any, type?: ical_type) { - this.setPropertyInternal(key, value, type); + this.#setPropertyInternal(key, value, type); for (let el of this.registered) { el.redraw(this); @@ -286,7 +335,7 @@ class VEvent { */ setProperties(pairs: [string, any, ical_type?][]) { for (let pair of pairs) { - this.setPropertyInternal(...pair); + this.#setPropertyInternal(...pair); } for (let el of this.registered) { el.redraw(this); @@ -307,6 +356,11 @@ class VEvent { } } + /** + Get the name of the containing calendar for this component. + + This is only valid for VEVENT components (I think) + */ get calendar(): string | null { return this.#calendar; } @@ -354,6 +408,7 @@ class VEvent { } } +/** Helper procedure when converting xml to vcal */ function make_vevent_value(value_tag: Element): VEventValue { /* TODO parameters */ return new VEventValue( @@ -367,23 +422,46 @@ function make_vevent_value(value_tag: Element): VEventValue { +/** Different frequency internals for recurrence rules. */ type freqType = 'SECONDLY' | 'MINUTELY' | 'HOURLY' | 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'YEARLY' + +/** Alternatives for when a week start, for recurrence rules */ type weekday = 'MO' | 'TU' | 'WE' | 'TH' | 'FR' | 'SA' | 'SU' +/** + A recurrence rule. + + The basic semantics of this class is borrowed from RFC 5545, and maps 1-to-1 + on those instances. See individual fields for mappings. + */ class RecurrenceRule { + /** The type of frequency of this rule */ freq?: freqType + /** Final instance of this rule. */ until?: Date + /** Maximum number of recurrences for this rule */ count?: number + /** The multiplier to `freq` */ interval?: number + /** Which seconds are relevant for this rule */ bysecond?: number[] + /** Which minutes are relevant for this rule */ byminute?: number[] + /** Which hours are relevant for this rule */ byhour?: number[] + /** Which weekday or weekday offsets are relevant for this rule */ byday?: (weekday | [number, weekday])[] + /** Which month days are relevant for this rule */ bymonthday?: number[] + /** Which year days are relevant for this rule */ byyearday?: number[] + /** Which week number are relevant for this rule */ byweekno?: number[] + /** Which months relevant for this rule (interval 1-12) */ bymonth?: number[] + /** TODO see the RFC */ bysetpos?: number[] + /** Which day the week start, according to this rule */ wkst?: weekday /** Converts ourselves to JCal data. */ diff --git a/static/tsconfig.json b/static/tsconfig.json index 02d475f3..352c8ab5 100644 --- a/static/tsconfig.json +++ b/static/tsconfig.json @@ -3,18 +3,17 @@ /* Visit https://aka.ms/tsconfig.json to read more about this file */ /* Projects */ /* Language and Environment */ - "target": "es2017", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. *//* Modules */ - "module": "CommonJS", /* Specify what module code is generated. *//* JavaScript Support */ - "allowJs": false, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. *//* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + "target": "es2017", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ /* Modules */ + "module": "CommonJS", /* Specify what module code is generated. */ /* JavaScript Support */ + "allowJs": false, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ /* Emit */// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ // "declarationMap": true, /* Create sourcemaps for d.ts files. */ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ "sourceMap": true, /* Create source map files for emitted JavaScript files. */ "newLine": "lf", /* Set the newline character for emitting files. */ - "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. *//* Interop Constraints */ + "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ /* Interop Constraints */ "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. *//* Type Checking */ - "strict": true, /* Enable all strict type-checking options. *//* Completeness */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ /* Completeness */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, "include": [ @@ -36,6 +35,9 @@ "@mxssfd/typedoc-theme" ], "theme": "my-theme", + "validation": { + "notDocumented": true + }, "out": "docs" } } \ No newline at end of file -- cgit v1.2.3