From f77997f6337bd8d21af9544ab21eb4b783a3a1a2 Mon Sep 17 00:00:00 2001 From: Misode Date: Tue, 26 May 2020 12:33:56 +0200 Subject: [PATCH] Add range node + default mods --- src/app/app.ts | 7 ++- src/model/DataModel.ts | 1 - src/nodes/AbstractNode.ts | 23 +++++--- src/nodes/BooleanNode.ts | 4 +- src/nodes/EnumNode.ts | 2 +- src/nodes/ListNode.ts | 4 +- src/nodes/MapNode.ts | 5 +- src/nodes/NumberNode.ts | 6 +- src/nodes/ObjectNode.ts | 4 +- src/nodes/StringNode.ts | 2 +- src/nodes/custom/RangeNode.ts | 101 ++++++++++++++++++++++++++++++++++ src/view/TreeView.ts | 12 +++- 12 files changed, 146 insertions(+), 25 deletions(-) create mode 100644 src/nodes/custom/RangeNode.ts diff --git a/src/app/app.ts b/src/app/app.ts index e83d98f4..45fcf789 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -9,6 +9,7 @@ import { ListNode } from '../nodes/ListNode' import { BooleanNode } from '../nodes/BooleanNode' import { MapNode } from '../nodes/MapNode' import { NumberNode } from '../nodes/NumberNode' +import { RangeNode } from '../nodes/custom/RangeNode' const EntityCollection = ['sheep', 'pig'] @@ -17,15 +18,15 @@ const predicateTree = new RootNode('predicate', { transform: (s: string) => (s === 'foo') ? {test: 'baz'} : s }), number: new NumberNode({integer: false, min: 0}), + range: new RangeNode(), predicate: new ObjectNode({ type: new EnumNode(EntityCollection), nbt: new StringNode(), test: new BooleanNode(), recipes: new MapNode( new StringNode(), - new ObjectNode({ - duration: new StringNode(), - flag: new BooleanNode() + new RangeNode({ + default: (v) => RangeNode.isExact(v) ? 2 : v }) ) }), diff --git a/src/model/DataModel.ts b/src/model/DataModel.ts index 4e6b2b03..0e03a59f 100644 --- a/src/model/DataModel.ts +++ b/src/model/DataModel.ts @@ -21,7 +21,6 @@ export class DataModel { } invalidate() { - console.log(this.data) this.listeners.forEach(listener => listener.invalidated(this)) } diff --git a/src/nodes/AbstractNode.ts b/src/nodes/AbstractNode.ts index 7fc4f55d..1b582eeb 100644 --- a/src/nodes/AbstractNode.ts +++ b/src/nodes/AbstractNode.ts @@ -2,9 +2,12 @@ import { DataModel } from "../model/DataModel" import { Path } from "../model/Path" import { TreeView } from "../view/TreeView" +export type IDefault = (value?: T) => T | undefined +export type ITransform = (value: T) => any + export interface INode { setParent: (parent: INode) => void - default: () => T + default: IDefault transform: (value: T) => any render: (path: Path, value: T, view: TreeView, options?: RenderOptions) => string renderRaw: (path: Path, value: T, view: TreeView, options?: RenderOptions) => string @@ -24,18 +27,20 @@ export type NodeChildren = { } export interface NodeMods { - default?: () => T + default?: IDefault transform?: (value: T) => any } export abstract class AbstractNode implements INode { parent?: INode - defaultMod: () => T - transformMod: (v: T) => T + defaultMod: IDefault + transformMod: ITransform - constructor(def: () => T, mods?: NodeMods) { - this.defaultMod = mods?.default ? mods.default : def - this.transformMod = mods?.transform ? mods.transform : (v: T) => v + constructor(mods?: NodeMods, defaultMods?: NodeMods, ) { + this.defaultMod = mods?.default ? mods.default : + defaultMods?.default ? defaultMods.default : (v) => v + this.transformMod = mods?.transform ? mods.transform : + defaultMods?.transform ? defaultMods.transform : (v) => v } setParent(parent: INode) { @@ -51,8 +56,8 @@ export abstract class AbstractNode implements INode { updateModel(el: Element, path: Path, model: DataModel) {} - default(): T { - return this.defaultMod() + default(value?: T) { + return this.defaultMod(value) } transform(value: T) { diff --git a/src/nodes/BooleanNode.ts b/src/nodes/BooleanNode.ts index e74e9316..d8d4a767 100644 --- a/src/nodes/BooleanNode.ts +++ b/src/nodes/BooleanNode.ts @@ -10,7 +10,9 @@ export class BooleanNode extends AbstractNode { force: boolean constructor(mods?: BooleanNodeMods) { - super(() => false, mods) + super(mods, { + default: () => false + }) this.force = (mods?.force === true) } diff --git a/src/nodes/EnumNode.ts b/src/nodes/EnumNode.ts index 68be30be..bc16e65e 100644 --- a/src/nodes/EnumNode.ts +++ b/src/nodes/EnumNode.ts @@ -7,7 +7,7 @@ export class EnumNode extends AbstractNode implements StateNode protected options: string[] constructor(options: string[], mods?: NodeMods) { - super(() => '', mods) + super(mods) this.options = options } diff --git a/src/nodes/ListNode.ts b/src/nodes/ListNode.ts index 5bd6ef1d..99bf6b34 100644 --- a/src/nodes/ListNode.ts +++ b/src/nodes/ListNode.ts @@ -8,7 +8,9 @@ export class ListNode extends AbstractNode { protected children: INode constructor(values: INode, mods?: NodeMods) { - super(() => [], mods) + super(mods, { + default: () => [] + }) this.children = values } diff --git a/src/nodes/MapNode.ts b/src/nodes/MapNode.ts index 48a1b2b4..8c6b5394 100644 --- a/src/nodes/MapNode.ts +++ b/src/nodes/MapNode.ts @@ -1,5 +1,4 @@ import { AbstractNode, NodeMods, INode, StateNode } from './AbstractNode' -import { DataModel } from '../model/DataModel' import { TreeView } from '../view/TreeView' import { Path } from '../model/Path' import { IObject } from './ObjectNode' @@ -13,7 +12,9 @@ export class MapNode extends AbstractNode { protected values: INode constructor(keys: StateNode, values: INode, mods?: NodeMods) { - super(() => ({}), mods) + super(mods, { + default: () => ({}) + }) this.keys = keys this.values = values } diff --git a/src/nodes/NumberNode.ts b/src/nodes/NumberNode.ts index c11647ad..fdede5a5 100644 --- a/src/nodes/NumberNode.ts +++ b/src/nodes/NumberNode.ts @@ -15,8 +15,9 @@ export class NumberNode extends AbstractNode implements StateNode 0, mods) - console.log(mods) + super(mods, { + default: () => 0 + }) this.integer = mods?.integer ? mods.integer : false this.min = mods?.min !== undefined ? mods.min : -Infinity this.max = mods?.max !== undefined ? mods.max : Infinity @@ -25,7 +26,6 @@ export class NumberNode extends AbstractNode implements StateNode this.max) return this.max return parsed diff --git a/src/nodes/ObjectNode.ts b/src/nodes/ObjectNode.ts index aed59487..651280f6 100644 --- a/src/nodes/ObjectNode.ts +++ b/src/nodes/ObjectNode.ts @@ -10,7 +10,9 @@ export class ObjectNode extends AbstractNode { protected fields: NodeChildren constructor(fields: NodeChildren, mods?: NodeMods) { - super(() => ({}), mods) + super(mods, { + default: () => ({}) + }) this.fields = fields Object.values(fields).forEach(child => { child.setParent(this) diff --git a/src/nodes/StringNode.ts b/src/nodes/StringNode.ts index bdf85101..7406080e 100644 --- a/src/nodes/StringNode.ts +++ b/src/nodes/StringNode.ts @@ -5,7 +5,7 @@ import { TreeView } from '../view/TreeView' export class StringNode extends AbstractNode implements StateNode { constructor(mods?: NodeMods) { - super(() => '', mods) + super(mods) } getState(el: Element) { diff --git a/src/nodes/custom/RangeNode.ts b/src/nodes/custom/RangeNode.ts new file mode 100644 index 00000000..f7c5091f --- /dev/null +++ b/src/nodes/custom/RangeNode.ts @@ -0,0 +1,101 @@ +import { AbstractNode, NodeMods, RenderOptions, StateNode } from '../AbstractNode' +import { Path } from '../../model/Path' +import { DataModel } from '../../model/DataModel' +import { TreeView } from '../../view/TreeView' + +export type IRange = number + | { min?: number, max?: number, type?: 'uniform' } + | { n?: number, p?: number, type: 'binomial' } + +export interface RangeNodeMods extends NodeMods { + integer?: boolean +} + +export class RangeNode extends AbstractNode implements StateNode { + integer: boolean + + constructor(mods?: RangeNodeMods) { + super(mods) + this.integer = mods?.integer ? mods.integer : false + } + + parseNumber(str: string): number { + return this.integer ? parseInt(str) : parseFloat(str) + } + + getState(el: Element): IRange { + const type = el.querySelector('select')!.value + if (type === 'exact') { + return this.parseNumber(el.querySelector('input')!.value) + } + if (type === 'range') { + const min = this.parseNumber(el.querySelectorAll('input')[0].value) + const max = this.parseNumber(el.querySelectorAll('input')[1].value) + return { + min: isNaN(min) ? undefined : min, + max: isNaN(max) ? undefined : max + } + } + const n = parseInt(el.querySelectorAll('input')[0].value) + const p = parseFloat(el.querySelectorAll('input')[1].value) + return { + type: 'binomial', + n: isNaN(n) ? undefined : n, + p: isNaN(p) ? undefined : p + } + } + + updateModel(el: Element, path: Path, model: DataModel) { + model.set(path, this.getState(el)) + } + + renderRaw(path: Path, value: IRange, view: TreeView, options?: RenderOptions) { + let curType = '' + let input = '' + if (value === undefined || typeof value === 'number') { + curType = 'exact' + input = `` + } else if (value.type === 'binomial') { + curType = 'binomial' + input = ` + + + ` + } else { + curType = 'range' + input = ` + + + ` + } + const id = view.register(el => { + (el as HTMLInputElement).value = curType + el.addEventListener('change', evt => { + const target = (el as HTMLInputElement).value + const newValue = this.default(target === 'exact' ? undefined : + target === 'binomial' ? {type: 'binomial'} : {}) + view.model.set(path, newValue) + evt.stopPropagation() + }) + }) + return `${options?.hideLabel ? `` : ``} + + ${input}` + } + + static isExact(v?: IRange) { + return v === undefined || typeof v === 'number' + } + + static isRange(v?: IRange) { + return v !== undefined && typeof v !== 'number' && v.type !== 'binomial' + } + + static isBinomial(v?: IRange) { + return v !== undefined && typeof v !== 'number' && v.type === 'binomial' + } +} diff --git a/src/view/TreeView.ts b/src/view/TreeView.ts index b2618b5b..ac0aa372 100644 --- a/src/view/TreeView.ts +++ b/src/view/TreeView.ts @@ -31,15 +31,23 @@ export class TreeView implements ModelListener { return id } - registerClick(callback: (el: Element) => void): string { + registerEvent(type: string, callback: (el: Element) => void): string { return this.register(el => { - el.addEventListener('click', evt => { + el.addEventListener(type, evt => { callback(el) evt.stopPropagation() }) }) } + registerChange(callback: (el: Element) => void): string { + return this.registerEvent('change', callback) + } + + registerClick(callback: (el: Element) => void): string { + return this.registerEvent('click', callback) + } + render() { this.target.innerHTML = this.model.schema.render(new Path(), this.model.data, this) for (const id in this.registry) {