mirror of
https://github.com/misode/misode.github.io.git
synced 2026-04-23 23:27:09 +00:00
Add map node and refactor defaults
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
45
src/nodes/MapNode.ts
Normal 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>`
|
||||
}
|
||||
}
|
||||
@@ -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>`}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user