Support styling

This commit is contained in:
Misode
2020-05-29 01:36:52 +02:00
parent 95d36cd8be
commit a8fabd555b
15 changed files with 143 additions and 27 deletions

View File

@@ -4,6 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="./styles/nodes.css">
</head>
<body>
<div id="header"></div>

35
public/styles/nodes.css Normal file
View File

@@ -0,0 +1,35 @@
.node {
font-size: 18px;
font-family: Arial, Helvetica, sans-serif
}
.list-entry:not(:last-child),
.map-entry:not(:last-child),
.object-node:not(:last-child) > .object-fields {
margin-bottom: 0.3rem;
}
.list-entry,
.map-entry,
.object-fields {
padding-left: 0.7rem;
}
.list-entry {
border-left: 2px solid #0b552a;
}
.map-fields {
border-left: 2px solid #066fb4;
}
.object-fields {
border-left: 2px solid #b9b9b9;
}
.object-node > label {
user-select: none;
}
.object-node > label.collapse {
cursor: pointer;
}

View File

@@ -51,5 +51,5 @@ export const SandboxSchema = new ObjectNode({
nbt: 'hi'
}
}),
transform: (v) => v?.condition === 'foo' ? ({...v, test: 'hello'}) : v
transform: (v: any) => v?.condition === 'foo' ? ({...v, test: 'hello'}) : v
});

View File

@@ -87,6 +87,10 @@ export class RangeNode extends AbstractNode<IRange> implements StateNode<IRange>
${input}`
}
getClassName() {
return 'range-node'
}
static isExact(v?: IRange) {
return v === undefined || typeof v === 'number'
}

View File

@@ -37,4 +37,8 @@ export class ResourceNode extends EnumNode {
return super.renderRaw(path, value, view, options)
}
}
getClassName() {
return 'enum-node'
}
}

View File

@@ -53,7 +53,7 @@ SCHEMAS.register('location-predicate', new ObjectNode({
x: new RangeNode(),
y: new RangeNode(),
z: new RangeNode()
}),
}, {collapse: true}),
biome: new ResourceNode(COLLECTIONS.get('biomes')),
feature: new EnumNode(COLLECTIONS.get('structures')),
dimension: new ResourceNode(COLLECTIONS.get('dimensions'), {additional: true}),
@@ -61,8 +61,8 @@ SCHEMAS.register('location-predicate', new ObjectNode({
light: new RangeNode()
}),
smokey: new BooleanNode(),
block: new ReferenceNode('block-predicate'),
fluid: new ReferenceNode('fluid-predicate')
block: new ReferenceNode('block-predicate', {collapse: true}),
fluid: new ReferenceNode('fluid-predicate', {collapse: true})
}))
SCHEMAS.register('statistic-predicate', new ObjectNode({
@@ -87,7 +87,7 @@ SCHEMAS.register('player-predicate', new ObjectNode({
)
}))
SCHEMAS.register('status-effect', new ObjectNode({
SCHEMAS.register('status-effect-predicate', new ObjectNode({
amplifier: new RangeNode(),
duration: new RangeNode(),
ambient: new BooleanNode(),
@@ -106,22 +106,22 @@ SCHEMAS.register('entity-predicate', new ObjectNode({
type: new StringNode(),
nbt: new StringNode(),
team: new StringNode(),
location: new ReferenceNode('location-predicate'),
distance: new ReferenceNode('distance-predicate'),
location: new ReferenceNode('location-predicate', {collapse: true}),
distance: new ReferenceNode('distance-predicate', {collapse: true}),
flags: new ObjectNode({
is_on_fire: new BooleanNode(),
is_sneaking: new BooleanNode(),
is_sprinting: new BooleanNode(),
is_swimming: new BooleanNode(),
is_baby: new BooleanNode()
}),
}, {collapse: true}),
equipment: new MapNode(
new EnumNode(COLLECTIONS.get('slots')),
new ReferenceNode('item-predicate')
),
// vehicle: new ReferenceNode('entity-predicate'),
// targeted_entity: new ReferenceNode('entity-predicate'),
player: new ReferenceNode('player-predicate'),
vehicle: new ReferenceNode('entity-predicate', {collapse: true}),
targeted_entity: new ReferenceNode('entity-predicate', {collapse: true}),
player: new ReferenceNode('player-predicate', {collapse: true}),
fishing_hook: new ObjectNode({
in_open_water: new BooleanNode()
}),

View File

@@ -17,6 +17,7 @@ export interface StateNode<T> extends INode<T> {
export type RenderOptions = {
hideLabel?: boolean
syncModel?: boolean
collapse?: boolean
}
export type NodeChildren = {
@@ -82,8 +83,12 @@ export abstract class AbstractNode<T> implements INode<T> {
const id = view.register(el => {
this.mounted(el, path, view)
})
return `<div data-id="${id}">${this.renderRaw(path, value, view, options)}</div>`
return `<div data-id="${id}" class="node ${this.getClassName()}">
${this.renderRaw(path, value, view, options)}
</div>`
}
abstract renderRaw(path: Path, value: T, view: TreeView, options?: RenderOptions): string
abstract getClassName(): string
}

View File

@@ -21,4 +21,8 @@ export class BooleanNode extends AbstractNode<boolean> {
<button${value === false ? ' style="font-weight: bold"' : ' '} data-id="${falseButton}">False</button>
<button${value === true ? ' style="font-weight: bold"' : ' '} data-id="${trueButton}">True</button>`
}
getClassName() {
return 'boolean-node'
}
}

View File

@@ -5,6 +5,7 @@ import { Path } from '../model/Path'
export class EnumNode extends AbstractNode<string> implements StateNode<string> {
protected options: string[]
static className = 'enum-node'
constructor(options: string[], mods?: NodeMods<string> | string) {
super(typeof mods === 'string' ? {
@@ -29,4 +30,8 @@ export class EnumNode extends AbstractNode<string> implements StateNode<string>
${this.options.map(o => `<option value="${o}">${o}</option>`).join('')}
</select>`
}
getClassName() {
return 'enum-node'
}
}

View File

@@ -33,7 +33,7 @@ export class ListNode extends AbstractNode<IObject[]> {
})
return `<label>${path.last()}:</label>
<button data-id="${button}">Add</button>
<div style="padding-left:8px">
<div class="list-fields">
${value.map((obj, index) => {
return this.renderEntry(path.push(index), obj, view)
}).join('')}
@@ -44,8 +44,12 @@ export class ListNode extends AbstractNode<IObject[]> {
const button = view.registerClick(el => {
view.model.set(path, undefined)
})
return `<div><button data-id="${button}">Remove</button>
return `<div class="list-entry"><button data-id="${button}">Remove</button>
${this.children.render(path, value, view, {hideLabel: true})}
</div>`
}
getClassName() {
return 'list-node'
}
}

View File

@@ -37,7 +37,7 @@ export class MapNode extends AbstractNode<IMap> {
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">
<div class="map-fields">
${Object.keys(value).map(key => {
return this.renderEntry(path.push(key), value[key], view)
}).join('')}
@@ -48,8 +48,12 @@ export class MapNode extends AbstractNode<IMap> {
const button = view.registerClick(el => {
view.model.set(path, undefined)
})
return `<div><button data-id="${button}">Remove</button>
return `<div class="map-entry"><button data-id="${button}">Remove</button>
${this.values.render(path, value, view)}
</div>`
}
getClassName() {
return 'map-node'
}
}

View File

@@ -39,4 +39,8 @@ export class NumberNode extends AbstractNode<number> implements StateNode<number
return `${options?.hideLabel ? `` : `<label>${path.last()}</label>`}
<input value="${value ?? ''}">`
}
getClassName() {
return 'number-node'
}
}

View File

@@ -19,15 +19,21 @@ export type FilteredChildren = {
[Case]?: NestedNodeChildren
}
export interface ObjectNodeMods extends NodeMods<object> {
collapse?: boolean
}
export class ObjectNode extends AbstractNode<IObject> {
fields: NodeChildren
cases: NestedNodeChildren
filter?: string
collapse?: boolean
constructor(fields: FilteredChildren, mods?: NodeMods<IObject>) {
constructor(fields: FilteredChildren, mods?: ObjectNodeMods) {
super({
default: () => ({}),
...mods})
this.collapse = mods?.collapse ?? false
const {[Switch]: _switch, [Case]: _case, ..._fields} = fields
this.fields = _fields
this.cases = _case ?? {}
@@ -46,14 +52,38 @@ export class ObjectNode extends AbstractNode<IObject> {
}
renderRaw(path: Path, value: IObject, view: TreeView, options?: RenderOptions) {
value = value ?? {}
const activeCase = this.filter ? this.cases[value[this.filter]] : {};
const activeFields = {...this.fields, ...activeCase}
return `${options?.hideLabel ? `` : `<label>${path.last()}:</label>
<div style="padding-left:8px">`}
${Object.keys(activeFields).map(f => {
return activeFields[f].render(path.push(f), value[f], view)
}).join('')}
${options?.hideLabel ? `` : `</div>`}`
if (options?.hideLabel) {
value = value ?? {}
return this.renderFields(path, value, view, activeFields)
} else if (this.collapse || options?.collapse) {
if (value === undefined) {
const id = view.registerClick(() => view.model.set(path, this.default()))
return `<label class="collapse closed" data-id="${id}">${path.last()}</label>`
} else {
const id = view.registerClick(() => view.model.set(path, undefined))
return `<label class="collapse open" data-id="${id}">${path.last()}</label>
<div class="object-fields">
${this.renderFields(path, value, view, activeFields)}
</div>`
}
} else {
value = value ?? {}
return `<label>${path.last()}</label>
<div class="object-fields">
${this.renderFields(path, value, view, activeFields)}
</div>`
}
}
renderFields(path: Path, value: IObject, view: TreeView, activeFields: NodeChildren) {
return Object.keys(activeFields).map(f => {
return activeFields[f].render(path.push(f), value[f], view)
}).join('')
}
getClassName() {
return 'object-node'
}
}

View File

@@ -3,11 +3,19 @@ import { TreeView } from '../view/TreeView'
import { Path } from '../model/Path'
import { SCHEMAS } from '../Registries'
export interface AnyNodeMods extends NodeMods<any> {
[name: string]: any
}
export class ReferenceNode extends AbstractNode<any> {
protected reference: () => INode<any>
options: RenderOptions
constructor(id: string, mods?: NodeMods<any>) {
constructor(id: string, mods?: AnyNodeMods) {
super(mods)
this.options = {
collapse: mods?.collapse
}
this.reference = () => SCHEMAS.get(id)
}
@@ -20,10 +28,14 @@ export class ReferenceNode extends AbstractNode<any> {
}
render(path: Path, value: any, view: TreeView, options?: RenderOptions) {
return this.reference()?.render(path, value, view, options)
return this.reference()?.render(path, value, view, {...this.options, ...options})
}
renderRaw(path: Path, value: any, view: TreeView, options?: RenderOptions) {
return this.reference()?.renderRaw(path, value, view, options)
return this.reference()?.renderRaw(path, value, view, {...this.options, ...options})
}
getClassName() {
return ''
}
}

View File

@@ -28,4 +28,8 @@ export class StringNode extends AbstractNode<string> implements StateNode<string
return `${options?.hideLabel ? `` : `<label>${path.last()}</label>`}
<input value="${value ?? ''}">`
}
getClassName() {
return 'string-node'
}
}