diff --git a/src/app/app.ts b/src/app/app.ts index 35d740fc..29871aeb 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -7,6 +7,7 @@ import { TreeView } from '../view/TreeView' import { SourceView } from '../view/SourceView' import { ListNode } from '../nodes/ListNode' import { BooleanNode } from '../nodes/BooleanNode' +import { MapNode } from '../nodes/MapNode' const EntityCollection = ['sheep', 'pig'] @@ -17,7 +18,14 @@ const predicateTree = new RootNode('predicate', { predicate: new ObjectNode({ type: new EnumNode(EntityCollection), nbt: new StringNode(), - test: new BooleanNode() + test: new BooleanNode(), + recipes: new MapNode( + new StringNode(), + new ObjectNode({ + duration: new StringNode(), + flag: new BooleanNode() + }) + ) }), effects: new ListNode( new ObjectNode({ diff --git a/src/model/DataModel.ts b/src/model/DataModel.ts index 83a15462..7373db96 100644 --- a/src/model/DataModel.ts +++ b/src/model/DataModel.ts @@ -24,18 +24,14 @@ export class DataModel { this.listeners.forEach(listener => listener.invalidated(this)) } - get(path: Path) { + set(path: Path, value: any) { let node = this.data; - for (let index of path) { - if (node === undefined) node = {} + for (let index of path.pop()) { + if (node[index] === undefined) { + node[index] = {} + } node = node[index] } - return node - } - - set(path: Path, value: any) { - let node = this.get(path.pop()) - if (node === undefined) node = {} console.log('Set', path.toString(), JSON.stringify(value)) diff --git a/src/nodes/AbstractNode.ts b/src/nodes/AbstractNode.ts index 1e4771e9..f00fce17 100644 --- a/src/nodes/AbstractNode.ts +++ b/src/nodes/AbstractNode.ts @@ -7,42 +7,41 @@ export interface INode { default: () => T | null transform: (value: T) => any render: (path: Path, value: T, view: TreeView, options?: RenderOptions) => string + renderRaw: (path: Path, value: T, view: TreeView, options?: RenderOptions) => string +} + +export interface StateNode extends INode { + getState: (el: Element) => T } export type RenderOptions = { hideLabel?: boolean + syncModel?: boolean } export type NodeChildren = { [name: string]: INode } -export type NodeMods = { +export interface NodeMods { default?: () => T transform?: (value: T) => any } export abstract class AbstractNode implements INode { parent?: INode - default: () => T | null = () => null - transformMod = (v: T) => v + defaultMod: () => T | null + transformMod: (v: T) => T - constructor(mods?: NodeMods) { - if (mods?.default) this.default = mods.default - if (mods?.transform) this.transformMod = mods.transform + constructor(mods?: NodeMods, def?: () => T | null) { + this.defaultMod = mods?.default ? mods.default : def ? def : () => null + this.transformMod = mods?.transform ? mods.transform : (v: T) => v } setParent(parent: INode) { this.parent = parent } - wrap(path: Path, view: TreeView, renderResult: string): string { - const id = view.register(el => { - this.mounted(el, path, view) - }) - return `
${renderResult}
` - } - mounted(el: Element, path: Path, view: TreeView) { el.addEventListener('change', evt => { this.updateModel(el, path, view.model) @@ -52,9 +51,20 @@ export abstract class AbstractNode implements INode { updateModel(el: Element, path: Path, model: DataModel) {} + default(): T | null { + return this.defaultMod() + } + transform(value: T) { return this.transformMod(value) } - abstract render(path: Path, value: T, view: TreeView, options?: any): string + render(path: Path, value: T, view: TreeView, options?: RenderOptions): string { + const id = view.register(el => { + this.mounted(el, path, view) + }) + return `
${this.renderRaw(path, value, view, options)}
` + } + + abstract renderRaw(path: Path, value: T, view: TreeView, options?: RenderOptions): string } diff --git a/src/nodes/BooleanNode.ts b/src/nodes/BooleanNode.ts index 1bfc2d42..c966f437 100644 --- a/src/nodes/BooleanNode.ts +++ b/src/nodes/BooleanNode.ts @@ -2,21 +2,27 @@ import { AbstractNode, NodeMods, RenderOptions } from "./AbstractNode"; import { Path } from "../model/Path"; import { TreeView } from "../view/TreeView"; +export interface BooleanNodeMods extends NodeMods { + force: boolean +} + export class BooleanNode extends AbstractNode { - constructor(mods?: NodeMods) { - super(mods) + force: boolean + + constructor(mods?: BooleanNodeMods) { + super(mods, () => false) + this.force = (mods?.force === true) } - render(path: Path, value: boolean, view: TreeView, options?: RenderOptions) { + renderRaw(path: Path, value: boolean, view: TreeView, options?: RenderOptions) { const falseButton = view.registerClick(el => { - view.model.set(path, value === false ? undefined : false) + view.model.set(path, !this.force && value === false ? undefined : false) }) const trueButton = view.registerClick(el => { - view.model.set(path, value === true ? undefined : true) + view.model.set(path, !this.force && value === true ? undefined : true) }) - return this.wrap(path, view, ` - ${options?.hideLabel ? `` : ``} + return `${options?.hideLabel ? `` : ``} False - True`) + True` } } diff --git a/src/nodes/EnumNode.ts b/src/nodes/EnumNode.ts index 23178232..47740fe6 100644 --- a/src/nodes/EnumNode.ts +++ b/src/nodes/EnumNode.ts @@ -1,26 +1,29 @@ -import { AbstractNode, NodeMods, RenderOptions } from './AbstractNode' +import { AbstractNode, NodeMods, RenderOptions, StateNode } from './AbstractNode' import { DataModel } from '../model/DataModel' import { TreeView } from '../view/TreeView' import { Path } from '../model/Path' -export class EnumNode extends AbstractNode { +export class EnumNode extends AbstractNode implements StateNode { protected options: string[] constructor(options: string[], mods?: NodeMods) { - super(mods) + super(mods, () => '') this.options = options } - updateModel(el: Element, path: Path, model: DataModel) { - model.set(path, el.querySelector('select')?.value) + getState(el: Element) { + return el.querySelector('select')!.value } - render(path: Path, value: string, view: TreeView, options?: RenderOptions) { + updateModel(el: Element, path: Path, model: DataModel) { + model.set(path, this.getState(el)) + } + + renderRaw(path: Path, value: string, view: TreeView, options?: RenderOptions) { const id = view.register(el => (el as HTMLInputElement).value = value) - return this.wrap(path, view, ` - ${options?.hideLabel ? `` : ``} + return `${options?.hideLabel ? `` : ``} `) + ` } } diff --git a/src/nodes/ListNode.ts b/src/nodes/ListNode.ts index 0cacbf62..b3551396 100644 --- a/src/nodes/ListNode.ts +++ b/src/nodes/ListNode.ts @@ -4,13 +4,11 @@ import { TreeView } from '../view/TreeView' import { Path } from '../model/Path' import { IObject } from './ObjectNode' - - export class ListNode extends AbstractNode { protected children: INode constructor(values: INode, mods?: NodeMods) { - super(mods) + super(mods, () => []) this.children = values } @@ -18,18 +16,18 @@ export class ListNode extends AbstractNode { model.set(path, el.querySelector('select')?.value) } - render(path: Path, value: IObject[], view: TreeView) { + renderRaw(path: Path, value: IObject[], view: TreeView) { value = value || [] const button = view.registerClick(el => { view.model.set(path, [...value, this.children.default()]) }) - return this.wrap(path, view, ` + return `
${value.map((obj, index) => { return this.renderEntry(path.push(index), obj, view) }).join('')} -
`) + ` } private renderEntry(path: Path, value: IObject, view: TreeView) { diff --git a/src/nodes/MapNode.ts b/src/nodes/MapNode.ts new file mode 100644 index 00000000..62f6989b --- /dev/null +++ b/src/nodes/MapNode.ts @@ -0,0 +1,45 @@ +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' + +export type IMap = { + [name: string]: IObject +} + +export class MapNode extends AbstractNode { + protected keys: StateNode + protected values: INode + + constructor(keys: StateNode, values: INode, mods?: NodeMods) { + super(mods, () => ({})) + this.keys = keys + this.values = values + } + + renderRaw(path: Path, value: IMap, view: TreeView) { + value = value || [] + const button = view.registerClick(el => { + const key = this.keys.getState(el.parentElement!) + view.model.set(path.push(key), this.values.default()) + }) + return ` + ${this.keys.renderRaw(path, '', view, {hideLabel: true, syncModel: false})} + +
+ ${Object.keys(value).map(key => { + return this.renderEntry(path.push(key), value[key], view) + }).join('')} +
` + } + + private renderEntry(path: Path, value: IObject, view: TreeView) { + const button = view.registerClick(el => { + view.model.set(path, undefined) + }) + return `
+ ${this.values.render(path, value, view)} +
` + } +} diff --git a/src/nodes/ObjectNode.ts b/src/nodes/ObjectNode.ts index d6847a4f..0a61ee1a 100644 --- a/src/nodes/ObjectNode.ts +++ b/src/nodes/ObjectNode.ts @@ -2,7 +2,7 @@ import { AbstractNode, NodeChildren, NodeMods, RenderOptions } from './AbstractN import { TreeView } from '../view/TreeView' import { Path } from '../model/Path' -export interface IObject { +export type IObject = { [name: string]: any } @@ -10,7 +10,7 @@ export class ObjectNode extends AbstractNode { protected fields: NodeChildren constructor(fields: NodeChildren, mods?: NodeMods) { - super(mods) + super(mods, () => ({})) this.fields = fields Object.values(fields).forEach(child => { child.setParent(this) @@ -27,15 +27,13 @@ export class ObjectNode extends AbstractNode { return res; } - render(path: Path, value: IObject, view: TreeView, options?: RenderOptions) { + renderRaw(path: Path, value: IObject, view: TreeView, options?: RenderOptions) { if (value === undefined) return `` - return this.wrap(path, view, ` - ${options?.hideLabel ? `` : ` -
- `} + return `${options?.hideLabel ? `` : ` +
`} ${Object.keys(this.fields).map(f => { return this.fields[f].render(path.push(f), value[f], view) }).join('')} - ${options?.hideLabel ? `` : `
`}`) + ${options?.hideLabel ? `` : `
`}` } } diff --git a/src/nodes/StringNode.ts b/src/nodes/StringNode.ts index 6c26a904..b364f0e6 100644 --- a/src/nodes/StringNode.ts +++ b/src/nodes/StringNode.ts @@ -1,20 +1,23 @@ -import { AbstractNode, NodeMods, RenderOptions } from './AbstractNode' +import { AbstractNode, NodeMods, RenderOptions, StateNode } from './AbstractNode' import { Path } from '../model/Path' import { DataModel } from '../model/DataModel' import { TreeView } from '../view/TreeView' -export class StringNode extends AbstractNode { +export class StringNode extends AbstractNode implements StateNode { constructor(mods?: NodeMods) { - super(mods) + super(mods, () => '') + } + + getState(el: Element) { + return el.querySelector('input')!.value } updateModel(el: Element, path: Path, model: DataModel) { - model.set(path, el.querySelector('input')?.value) + model.set(path, this.getState(el)) } - render(path: Path, value: string, view: TreeView, options?: RenderOptions) { - return this.wrap(path, view, ` - ${options?.hideLabel ? `` : ``} - `) + renderRaw(path: Path, value: string, view: TreeView, options?: RenderOptions) { + return `${options?.hideLabel ? `` : ``} + ` } -} +} diff --git a/src/view/TreeView.ts b/src/view/TreeView.ts index 7d9b5829..b2618b5b 100644 --- a/src/view/TreeView.ts +++ b/src/view/TreeView.ts @@ -33,7 +33,10 @@ export class TreeView implements ModelListener { registerClick(callback: (el: Element) => void): string { return this.register(el => { - el.addEventListener('click', _ => callback(el)) + el.addEventListener('click', evt => { + callback(el) + evt.stopPropagation() + }) }) }