Add map node and refactor defaults

This commit is contained in:
Misode
2020-05-25 23:51:00 +02:00
parent 3620074c86
commit 387218193e
10 changed files with 135 additions and 65 deletions

View File

@@ -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({

View File

@@ -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))

View File

@@ -7,42 +7,41 @@ export interface INode<T> {
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<T> extends INode<T> {
getState: (el: Element) => T
}
export type RenderOptions = {
hideLabel?: boolean
syncModel?: boolean
}
export type NodeChildren = {
[name: string]: INode<any>
}
export type NodeMods<T> = {
export interface NodeMods<T> {
default?: () => T
transform?: (value: T) => any
}
export abstract class AbstractNode<T> implements INode<T> {
parent?: INode<any>
default: () => T | null = () => null
transformMod = (v: T) => v
defaultMod: () => T | null
transformMod: (v: T) => T
constructor(mods?: NodeMods<T>) {
if (mods?.default) this.default = mods.default
if (mods?.transform) this.transformMod = mods.transform
constructor(mods?: NodeMods<T>, def?: () => T | null) {
this.defaultMod = mods?.default ? mods.default : def ? def : () => null
this.transformMod = mods?.transform ? mods.transform : (v: T) => v
}
setParent(parent: INode<any>) {
this.parent = parent
}
wrap(path: Path, view: TreeView, renderResult: string): string {
const id = view.register(el => {
this.mounted(el, path, view)
})
return `<div data-id="${id}">${renderResult}</div>`
}
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<T> implements INode<T> {
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 `<div data-id="${id}">${this.renderRaw(path, value, view, options)}</div>`
}
abstract renderRaw(path: Path, value: T, view: TreeView, options?: RenderOptions): string
}

View File

@@ -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<boolean> {
force: boolean
}
export class BooleanNode extends AbstractNode<boolean> {
constructor(mods?: NodeMods<boolean>) {
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 ? `` : `<label>${path.last()}</label>`}
return `${options?.hideLabel ? `` : `<label>${path.last()}</label>`}
<button${value === false ? ' style="font-weight: bold"' : ' '} data-id="${falseButton}">False</button>
<button${value === true ? ' style="font-weight: bold"' : ' '} data-id="${trueButton}">True</button>`)
<button${value === true ? ' style="font-weight: bold"' : ' '} data-id="${trueButton}">True</button>`
}
}

View File

@@ -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<string> {
export class EnumNode extends AbstractNode<string> implements StateNode<string> {
protected options: string[]
constructor(options: string[], mods?: NodeMods<string>) {
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 ? `` : `<label>${path.last()}</label>`}
return `${options?.hideLabel ? `` : `<label>${path.last()}</label>`}
<select data-id=${id}>
${this.options.map(o => `<option value="${o}">${o}</option>`).join('')}
</select>`)
</select>`
}
}

View File

@@ -4,13 +4,11 @@ import { TreeView } from '../view/TreeView'
import { Path } from '../model/Path'
import { IObject } from './ObjectNode'
export class ListNode extends AbstractNode<IObject[]> {
protected children: INode<any>
constructor(values: INode<any>, mods?: NodeMods<IObject[]>) {
super(mods)
super(mods, () => [])
this.children = values
}
@@ -18,18 +16,18 @@ export class ListNode extends AbstractNode<IObject[]> {
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, `<label>${path.last()}:</label>
return `<label>${path.last()}:</label>
<button data-id="${button}">Add</button>
<div style="padding-left:8px">
${value.map((obj, index) => {
return this.renderEntry(path.push(index), obj, view)
}).join('')}
</div>`)
</div>`
}
private renderEntry(path: Path, value: IObject, view: TreeView) {

45
src/nodes/MapNode.ts Normal file
View File

@@ -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<IMap> {
protected keys: StateNode<string>
protected values: INode<any>
constructor(keys: StateNode<string>, values: INode<any>, mods?: NodeMods<IMap>) {
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 `<label>${path.last()}:</label>
${this.keys.renderRaw(path, '', view, {hideLabel: true, syncModel: false})}
<button data-id="${button}">Add</button>
<div style="padding-left:8px">
${Object.keys(value).map(key => {
return this.renderEntry(path.push(key), value[key], view)
}).join('')}
</div>`
}
private renderEntry(path: Path, value: IObject, view: TreeView) {
const button = view.registerClick(el => {
view.model.set(path, undefined)
})
return `<div><button data-id="${button}">Remove</button>
${this.values.render(path, value, view)}
</div>`
}
}

View File

@@ -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<IObject> {
protected fields: NodeChildren
constructor(fields: NodeChildren, mods?: NodeMods<IObject>) {
super(mods)
super(mods, () => ({}))
this.fields = fields
Object.values(fields).forEach(child => {
child.setParent(this)
@@ -27,15 +27,13 @@ export class ObjectNode extends AbstractNode<IObject> {
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 ? `` : `<label>${path.last()}:</label>
<div style="padding-left:8px">
`}
return `${options?.hideLabel ? `` : `<label>${path.last()}:</label>
<div style="padding-left:8px">`}
${Object.keys(this.fields).map(f => {
return this.fields[f].render(path.push(f), value[f], view)
}).join('')}
${options?.hideLabel ? `` : `</div>`}`)
${options?.hideLabel ? `` : `</div>`}`
}
}

View File

@@ -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<string> {
export class StringNode extends AbstractNode<string> implements StateNode<string> {
constructor(mods?: NodeMods<string>) {
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 ? `` : `<label>${path.last()}</label>`}
<input value="${value || ''}"></input>`)
renderRaw(path: Path, value: string, view: TreeView, options?: RenderOptions) {
return `${options?.hideLabel ? `` : `<label>${path.last()}</label>`}
<input value="${value || ''}"></input>`
}
}
}

View File

@@ -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()
})
})
}