mirror of
https://github.com/misode/misode.github.io.git
synced 2026-04-23 07:10:41 +00:00
Add documentation
This commit is contained in:
@@ -6,6 +6,9 @@ export interface Registry<T> {
|
||||
get(id: string): T
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry for schemas
|
||||
*/
|
||||
class SchemaRegistry implements Registry<INode<any>> {
|
||||
private registry: { [id: string]: INode<any> } = {}
|
||||
|
||||
@@ -22,6 +25,9 @@ class SchemaRegistry implements Registry<INode<any>> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry for collections
|
||||
*/
|
||||
class CollectionRegistry implements Registry<string[]> {
|
||||
private registry: { [id: string]: string[] } = {}
|
||||
|
||||
@@ -38,6 +44,9 @@ class CollectionRegistry implements Registry<string[]> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry for locales
|
||||
*/
|
||||
export interface Locale {
|
||||
[key: string]: string
|
||||
}
|
||||
@@ -46,6 +55,11 @@ class LocaleRegistry implements Registry<Locale> {
|
||||
private registry: { [id: string]: Locale } = {}
|
||||
language: string = ''
|
||||
|
||||
/**
|
||||
*
|
||||
* @param id locale identifier
|
||||
* @param locale object mapping keys to translations
|
||||
*/
|
||||
register(id: string, locale: Locale): void {
|
||||
this.registry[id] = locale
|
||||
}
|
||||
@@ -67,6 +81,14 @@ export const SCHEMAS = new SchemaRegistry()
|
||||
export const COLLECTIONS = new CollectionRegistry()
|
||||
export const LOCALES = new LocaleRegistry()
|
||||
|
||||
/**
|
||||
* Gets the locale of a key from the locale registry.
|
||||
*
|
||||
* @param key string or path that refers to a locale ID.
|
||||
* If a string is given, an exact match is required.
|
||||
* If a path is given, it finds the longest match at the end.
|
||||
* @returns undefined if the key isn't found for the selected language
|
||||
*/
|
||||
export const locale = (key: string | Path) => {
|
||||
if (typeof key === 'string') {
|
||||
return LOCALES.getLocale(key) ?? key
|
||||
|
||||
@@ -5,30 +5,52 @@ export interface ModelListener {
|
||||
invalidated(model: DataModel): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Holding the data linked to a given schema
|
||||
*/
|
||||
export class DataModel {
|
||||
data: any
|
||||
schema: INode<any>
|
||||
/** A list of listeners that want to be notified when the model is invalidated */
|
||||
listeners: ModelListener[]
|
||||
|
||||
/**
|
||||
* @param schema node to use as schema for this model
|
||||
*/
|
||||
constructor(schema: INode<any>) {
|
||||
this.schema = schema
|
||||
this.data = schema.default()
|
||||
this.listeners = []
|
||||
}
|
||||
|
||||
/**
|
||||
* @param listener the listener to notify when the model is invalidated
|
||||
*/
|
||||
addListener(listener: ModelListener) {
|
||||
this.listeners.push(listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* Force notify all listeners that the model is invalidated
|
||||
*/
|
||||
invalidate() {
|
||||
this.listeners.forEach(listener => listener.invalidated(this))
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the full data and notifies listeners
|
||||
* @param value new model data
|
||||
*/
|
||||
reset(value: any) {
|
||||
this.data = value
|
||||
this.invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the data at a specified path
|
||||
* @param path path at which to find the data
|
||||
* @returns undefined, if the the path does not exist in the data
|
||||
*/
|
||||
get(path: Path) {
|
||||
let node = this.data;
|
||||
for (let index of path) {
|
||||
@@ -38,6 +60,11 @@ export class DataModel {
|
||||
return node
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the date on a path. Node will be removed when value is undefined
|
||||
* @param path path to update
|
||||
* @param value new data at the specified path
|
||||
*/
|
||||
set(path: Path, value: any) {
|
||||
let node = this.data;
|
||||
for (let index of path.pop()) {
|
||||
|
||||
@@ -2,23 +2,41 @@ import { DataModel } from "./DataModel"
|
||||
|
||||
export type PathElement = (string | number)
|
||||
|
||||
/**
|
||||
* Immutable helper class to represent a path in data
|
||||
* @implements {Iterable<PathElement>}
|
||||
*/
|
||||
export class Path implements Iterable<PathElement> {
|
||||
private arr: PathElement[]
|
||||
model?: DataModel
|
||||
|
||||
/**
|
||||
* @param arr Initial array of path elements. Empty if not given
|
||||
* @param model Model attached to this path
|
||||
*/
|
||||
constructor(arr?: PathElement[], model?: DataModel) {
|
||||
this.arr = arr ?? []
|
||||
this.model = model
|
||||
}
|
||||
|
||||
/**
|
||||
* The last element of this path
|
||||
*/
|
||||
last(): PathElement {
|
||||
return this.arr[this.arr.length - 1]
|
||||
}
|
||||
|
||||
/**
|
||||
* A new path with the last element removed
|
||||
*/
|
||||
pop(): Path {
|
||||
return new Path(this.arr.slice(0, -1), this.model)
|
||||
}
|
||||
|
||||
/**
|
||||
* A new path with an element added at the end
|
||||
* @param element element to push at the end of the array
|
||||
*/
|
||||
push(element: PathElement): Path {
|
||||
return new Path([...this.arr, element], this.model)
|
||||
}
|
||||
@@ -31,10 +49,18 @@ export class Path implements Iterable<PathElement> {
|
||||
return this.arr
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches a model to this path and all paths created from this
|
||||
* @param model
|
||||
*/
|
||||
withModel(model: DataModel): Path {
|
||||
return new Path([...this.arr], model)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the data from the model if it was attached
|
||||
* @returns undefined, if no model was attached
|
||||
*/
|
||||
get(): any {
|
||||
return this.model?.get(this)
|
||||
}
|
||||
|
||||
@@ -2,8 +2,11 @@ import { DataModel } from "../model/DataModel"
|
||||
import { Path } from "../model/Path"
|
||||
import { TreeView } from "../view/TreeView"
|
||||
|
||||
/**
|
||||
* Schema node that supports some standard transformations
|
||||
*/
|
||||
export interface INode<T> {
|
||||
default: IDefault<T>
|
||||
default: (value?: T) => T | undefined
|
||||
transform: (path: Path, value: T) => any
|
||||
enabled: (path: Path, model: DataModel) => boolean
|
||||
render: (path: Path, value: T, view: TreeView, options?: RenderOptions) => string
|
||||
@@ -36,12 +39,20 @@ export interface NodeMods<T> {
|
||||
force?: IForce
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic implementation of the nodes
|
||||
*
|
||||
* h
|
||||
*/
|
||||
export abstract class AbstractNode<T> implements INode<T> {
|
||||
defaultMod: IDefault<T>
|
||||
transformMod: ITransform<T>
|
||||
enableMod: IEnable
|
||||
forceMod: IForce
|
||||
|
||||
/**
|
||||
* @param mods modifiers of the default transformations
|
||||
*/
|
||||
constructor(mods?: NodeMods<T>) {
|
||||
this.defaultMod = mods?.default ?? ((v) => v)
|
||||
this.transformMod = mods?.transform ?? ((v) => v)
|
||||
@@ -49,6 +60,12 @@ export abstract class AbstractNode<T> implements INode<T> {
|
||||
this.forceMod = mods?.force ?? (() => false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs when the element is mounted to the DOM
|
||||
* @param el mounted element
|
||||
* @param path data path that this node represents
|
||||
* @param view corresponding tree view where this was mounted
|
||||
*/
|
||||
mounted(el: Element, path: Path, view: TreeView) {
|
||||
el.addEventListener('change', evt => {
|
||||
this.updateModel(el, path, view.model)
|
||||
@@ -56,18 +73,37 @@ export abstract class AbstractNode<T> implements INode<T> {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs when the DOM element 'change' event is called
|
||||
* @param el mounted element
|
||||
* @param path data path that this node represents
|
||||
* @param model corresponding model
|
||||
*/
|
||||
updateModel(el: Element, path: Path, model: DataModel) {}
|
||||
|
||||
/**
|
||||
* The default value of this node
|
||||
* @param value optional original value
|
||||
*/
|
||||
default(value?: T) {
|
||||
return this.defaultMod(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the data model to the final output format
|
||||
* @param
|
||||
*/
|
||||
transform(path: Path, value: T) {
|
||||
if (!this.enabled(path)) return undefined
|
||||
if (value === undefined && this.force()) value = this.default(value)!
|
||||
return this.transformMod(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the node should be enabled for this path
|
||||
* @param path
|
||||
* @param model
|
||||
*/
|
||||
enabled(path: Path, model?: DataModel) {
|
||||
if (model) path = path.withModel(model)
|
||||
return this.enableMod(path.pop())
|
||||
@@ -77,6 +113,14 @@ export abstract class AbstractNode<T> implements INode<T> {
|
||||
return this.forceMod()
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps and renders the node
|
||||
* @param path location
|
||||
* @param value data used at
|
||||
* @param view tree view context, containing the model
|
||||
* @param options optional render options
|
||||
* @returns string HTML wrapped representation of this node using the given data
|
||||
*/
|
||||
render(path: Path, value: T, view: TreeView, options?: RenderOptions): string {
|
||||
if (!this.enabled(path, view.model)) return ''
|
||||
|
||||
@@ -88,7 +132,18 @@ export abstract class AbstractNode<T> implements INode<T> {
|
||||
</div>`
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the node and handles events to update the model
|
||||
* @param path
|
||||
* @param value
|
||||
* @param view tree view context, containing the model
|
||||
* @param options optional render options
|
||||
* @returns string HTML representation of this node using the given data
|
||||
*/
|
||||
abstract renderRaw(path: Path, value: T, view: TreeView, options?: RenderOptions): string
|
||||
|
||||
/**
|
||||
* The CSS classname used in the wrapped <div>
|
||||
*/
|
||||
abstract getClassName(): string
|
||||
}
|
||||
|
||||
@@ -3,8 +3,14 @@ import { Path } from "../model/Path";
|
||||
import { TreeView } from "../view/TreeView";
|
||||
import { locale } from "../Registries";
|
||||
|
||||
/**
|
||||
* Boolean node with two buttons for true/false
|
||||
*/
|
||||
export class BooleanNode extends AbstractNode<boolean> {
|
||||
|
||||
/**
|
||||
* @param mods optional node modifiers
|
||||
*/
|
||||
constructor(mods?: NodeMods<boolean>) {
|
||||
super({
|
||||
default: () => false,
|
||||
|
||||
@@ -4,10 +4,17 @@ import { TreeView } from '../view/TreeView'
|
||||
import { Path } from '../model/Path'
|
||||
import { locale } from '../Registries'
|
||||
|
||||
/**
|
||||
* Enum node that shows a list of options to choose from
|
||||
*/
|
||||
export class EnumNode extends AbstractNode<string> implements StateNode<string> {
|
||||
protected options: string[]
|
||||
static className = 'enum-node'
|
||||
|
||||
/**
|
||||
* @param options options to choose from in the select
|
||||
* @param mods optional node modifiers or a string to be the default value
|
||||
*/
|
||||
constructor(options: string[], mods?: NodeMods<string> | string) {
|
||||
super(typeof mods === 'string' ? {
|
||||
default: () => mods,
|
||||
|
||||
@@ -5,9 +5,16 @@ import { Path } from '../model/Path'
|
||||
import { IObject } from './ObjectNode'
|
||||
import { locale } from '../Registries'
|
||||
|
||||
/**
|
||||
* List node where children can be added and removed from
|
||||
*/
|
||||
export class ListNode extends AbstractNode<IObject[]> {
|
||||
protected children: INode<any>
|
||||
|
||||
/**
|
||||
* @param values node used for its children
|
||||
* @param mods optional node modifiers
|
||||
*/
|
||||
constructor(values: INode<any>, mods?: NodeMods<IObject[]>) {
|
||||
super({
|
||||
default: () => [],
|
||||
|
||||
@@ -8,10 +8,19 @@ export type IMap = {
|
||||
[name: string]: IObject
|
||||
}
|
||||
|
||||
/**
|
||||
* Map nodes similar to list nodes, but a string key is required to add children
|
||||
*/
|
||||
export class MapNode extends AbstractNode<IMap> {
|
||||
protected keys: StateNode<string>
|
||||
protected values: INode<any>
|
||||
|
||||
/**
|
||||
*
|
||||
* @param keys node used for the string key
|
||||
* @param values node used for the map values
|
||||
* @param mods optional node modifiers
|
||||
*/
|
||||
constructor(keys: StateNode<string>, values: INode<any>, mods?: NodeMods<IMap>) {
|
||||
super({
|
||||
default: () => ({}),
|
||||
|
||||
@@ -5,16 +5,25 @@ import { TreeView } from '../view/TreeView'
|
||||
import { locale } from '../Registries'
|
||||
|
||||
export interface NumberNodeMods extends NodeMods<number> {
|
||||
/** Whether numbers should be converted to integers on input */
|
||||
integer?: boolean
|
||||
/** If specified, number will be capped at this minimum */
|
||||
min?: number
|
||||
/** If specified, number will be capped at this maximum */
|
||||
max?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Configurable number node with one text field
|
||||
*/
|
||||
export class NumberNode extends AbstractNode<number> implements StateNode<number> {
|
||||
integer: boolean
|
||||
min: number
|
||||
max: number
|
||||
|
||||
/**
|
||||
* @param mods optional node modifiers
|
||||
*/
|
||||
constructor(mods?: NumberNodeMods) {
|
||||
super({
|
||||
default: () => 0,
|
||||
|
||||
@@ -16,20 +16,31 @@ export type IObject = {
|
||||
|
||||
export type FilteredChildren = {
|
||||
[name: string]: INode<any>
|
||||
/** The field to filter on */
|
||||
[Switch]?: string
|
||||
/** Map of filter values to node fields */
|
||||
[Case]?: NestedNodeChildren
|
||||
}
|
||||
|
||||
export interface ObjectNodeMods extends NodeMods<object> {
|
||||
/** Whether the object can be collapsed. Necessary when recursively nesting. */
|
||||
collapse?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Object node containing fields with different types.
|
||||
* Has the ability to filter fields based on a switch field.
|
||||
*/
|
||||
export class ObjectNode extends AbstractNode<IObject> {
|
||||
fields: NodeChildren
|
||||
cases: NestedNodeChildren
|
||||
filter?: string
|
||||
collapse?: boolean
|
||||
|
||||
/**
|
||||
* @param fields children containing the optional switch and case
|
||||
* @param mods optional node modifiers
|
||||
*/
|
||||
constructor(fields: FilteredChildren, mods?: ObjectNodeMods) {
|
||||
super({
|
||||
default: () => ({}),
|
||||
|
||||
@@ -7,10 +7,17 @@ export interface AnyNodeMods extends NodeMods<any> {
|
||||
[name: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* Reference node. Must be used when recursively adding nodes.
|
||||
*/
|
||||
export class ReferenceNode extends AbstractNode<any> {
|
||||
protected reference: () => INode<any>
|
||||
options: RenderOptions
|
||||
|
||||
/**
|
||||
* @param id schema id that was registered
|
||||
* @param mods optional node modifiers
|
||||
*/
|
||||
constructor(id: string, mods?: AnyNodeMods) {
|
||||
super(mods)
|
||||
this.options = {
|
||||
|
||||
@@ -5,12 +5,19 @@ import { TreeView } from '../view/TreeView'
|
||||
import { locale } from '../Registries'
|
||||
|
||||
export interface StringNodeMods extends NodeMods<string> {
|
||||
/** Whether the string can also be empty */
|
||||
allowEmpty?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple string node with one text field
|
||||
*/
|
||||
export class StringNode extends AbstractNode<string> implements StateNode<string> {
|
||||
allowEmpty: boolean
|
||||
|
||||
/**
|
||||
* @param mods optional node modifiers
|
||||
*/
|
||||
constructor(mods?: StringNodeMods) {
|
||||
super(mods)
|
||||
this.allowEmpty = mods?.allowEmpty ?? false
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import { DataModel, ModelListener } from "../model/DataModel"
|
||||
import { Path } from "../model/Path"
|
||||
|
||||
/**
|
||||
* JSON representation view of the model.
|
||||
* Renders the result in a <textarea>.
|
||||
*/
|
||||
export class SourceView implements ModelListener {
|
||||
model: DataModel
|
||||
target: HTMLElement
|
||||
|
||||
/**
|
||||
* @param model data model this view represents and listens to
|
||||
* @param target DOM element to render the view
|
||||
*/
|
||||
constructor(model: DataModel, target: HTMLElement) {
|
||||
this.model = model
|
||||
this.target = target
|
||||
@@ -25,6 +33,10 @@ export class SourceView implements ModelListener {
|
||||
this.target.appendChild(textarea)
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-renders the view
|
||||
* @override
|
||||
*/
|
||||
invalidated() {
|
||||
this.render()
|
||||
}
|
||||
|
||||
@@ -8,29 +8,50 @@ type Registry = {
|
||||
const registryIdLength = 12
|
||||
const dec2hex = (dec: number) => ('0' + dec.toString(16)).substr(-2)
|
||||
|
||||
/**
|
||||
* Helper function to generate a random ID
|
||||
*/
|
||||
export function getId() {
|
||||
var arr = new Uint8Array((registryIdLength || 40) / 2)
|
||||
window.crypto.getRandomValues(arr)
|
||||
return Array.from(arr, dec2hex).join('')
|
||||
}
|
||||
|
||||
/**
|
||||
* DOM representation view of the model.
|
||||
*/
|
||||
export class TreeView implements ModelListener {
|
||||
model: DataModel
|
||||
target: HTMLElement
|
||||
registry: Registry = {}
|
||||
|
||||
/**
|
||||
* @param model data model this view represents and listens to
|
||||
* @param target DOM element to render the view
|
||||
*/
|
||||
constructor(model: DataModel, target: HTMLElement) {
|
||||
this.model = model
|
||||
this.target = target
|
||||
model.addListener(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a callback and gives an ID
|
||||
* @param callback function that is called when the element is mounted
|
||||
* @returns the ID that should be applied to the data-id attribute
|
||||
*/
|
||||
register(callback: (el: Element) => void): string {
|
||||
const id = getId()
|
||||
this.registry[id] = callback
|
||||
return id
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an event and gives an ID
|
||||
* @param type event type
|
||||
* @param callback function that is called when the event is fired
|
||||
* @returns the ID that should be applied to the data-id attribute
|
||||
*/
|
||||
registerEvent(type: string, callback: (el: Element) => void): string {
|
||||
return this.register(el => {
|
||||
el.addEventListener(type, evt => {
|
||||
@@ -40,10 +61,20 @@ export class TreeView implements ModelListener {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a change event and gives an ID
|
||||
* @param callback function that is called when the event is fired
|
||||
* @returns the ID that should be applied to the data-id attribute
|
||||
*/
|
||||
registerChange(callback: (el: Element) => void): string {
|
||||
return this.registerEvent('change', callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a click event and gives an ID
|
||||
* @param callback function that is called when the event is fired
|
||||
* @returns the ID that should be applied to the data-id attribute
|
||||
*/
|
||||
registerClick(callback: (el: Element) => void): string {
|
||||
return this.registerEvent('click', callback)
|
||||
}
|
||||
@@ -57,6 +88,10 @@ export class TreeView implements ModelListener {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-renders the view
|
||||
* @override
|
||||
*/
|
||||
invalidated(model: DataModel) {
|
||||
this.render()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user