Use mcschema hooks beta and watch things explode

This commit is contained in:
Misode
2020-10-09 00:55:18 +02:00
parent ff6e02fb36
commit 8c610b99e0
9 changed files with 406 additions and 13 deletions

14
package-lock.json generated
View File

@@ -5,16 +5,16 @@
"requires": true,
"dependencies": {
"@mcschema/core": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/@mcschema/core/-/core-0.9.3.tgz",
"integrity": "sha512-DtDncu+NE32OUDzjCv+IAA+owPXASEbR9WXRCikpDY/eTxpjiUkgrsjYzG9A8GhE/Hw60vbwbDuSrHjeTDYSTg=="
"version": "0.10.0-beta.6",
"resolved": "https://registry.npmjs.org/@mcschema/core/-/core-0.10.0-beta.6.tgz",
"integrity": "sha512-52QcHOBuATSL9dqI3fuNMexuJFwwF9NrtILs+IzkwAt/40e7VnR29doJW8P3JkNcZ2a9BaVtw5ExfbePj9rTFg=="
},
"@mcschema/java-1.16": {
"version": "0.5.8",
"resolved": "https://registry.npmjs.org/@mcschema/java-1.16/-/java-1.16-0.5.8.tgz",
"integrity": "sha512-hbrDilc5fQK97x0MhbBiwHT+C4xCjsO7HPdgcZqvzhlsbcSkG2fz03Ew/myJJQyxYpShyZLq2QPl6OVtiCp40Q==",
"version": "0.5.9-beta.1",
"resolved": "https://registry.npmjs.org/@mcschema/java-1.16/-/java-1.16-0.5.9-beta.1.tgz",
"integrity": "sha512-Yh+Z6jH/GU/WgshsHfz6aRZLXGMldhaHgWfk+eWvY3f8ftrurNW+OUddCx3kwOZ4gADy5pivVhISB1yecpWKxQ==",
"requires": {
"@mcschema/core": "^0.9.0"
"@mcschema/core": "^0.10.0-beta.3"
}
},
"@mcschema/locales": {

View File

@@ -12,8 +12,8 @@
"author": "Misode",
"license": "MIT",
"dependencies": {
"@mcschema/core": "^0.9.3",
"@mcschema/java-1.16": "^0.5.8",
"@mcschema/core": "^0.10.0-beta.6",
"@mcschema/java-1.16": "^0.5.9-beta.1",
"@mcschema/locales": "^0.1.11",
"@types/google.analytics": "0.0.40",
"@types/split.js": "^1.4.0",

89
src/app/Mounter.ts Normal file
View File

@@ -0,0 +1,89 @@
import { ModelPath } from '@mcschema/core'
const dec2hex = (dec: number) => ('0' + dec.toString(16)).substr(-2)
export function hexId(length = 12) {
var arr = new Uint8Array(length / 2)
window.crypto.getRandomValues(arr)
return Array.from(arr, dec2hex).join('')
}
type Registry = {
[id: string]: (el: Element) => void
}
type NodeInjector = (path: ModelPath, mounter: Mounter) => string
type TreeViewOptions = {
nodeInjector?: NodeInjector
}
export interface Mounter {
register(callback: (el: Element) => void): string
registerEvent(type: string, callback: (el: Element) => void): string
registerChange(callback: (el: Element) => void): string
registerClick(callback: (el: Element) => void): string
nodeInjector(path: ModelPath, mounter: Mounter): string
mount(el: HTMLElement): void
}
export class Mounter implements Mounter {
registry: Registry = {}
nodeInjector: NodeInjector
constructor(options?: TreeViewOptions) {
this.nodeInjector = options?.nodeInjector ?? (() => '')
}
/**
* Registers a callback and gives an ID
* @param callback function that is called when the element is mounted
* @returns the ID that should be applied to the data-id attribute
*/
register(callback: (el: Element) => void): string {
const id = hexId()
this.registry[id] = callback
return id
}
/**
* Registers an event and gives an ID
* @param type event type
* @param callback function that is called when the event is fired
* @returns the ID that should be applied to the data-id attribute
*/
registerEvent(type: string, callback: (el: Element) => void): string {
return this.register(el => {
el.addEventListener(type, evt => {
callback(el)
evt.stopPropagation()
})
})
}
/**
* Registers a change event and gives an ID
* @param callback function that is called when the event is fired
* @returns the ID that should be applied to the data-id attribute
*/
registerChange(callback: (el: Element) => void): string {
return this.registerEvent('change', callback)
}
/**
* Registers a click event and gives an ID
* @param callback function that is called when the event is fired
* @returns the ID that should be applied to the data-id attribute
*/
registerClick(callback: (el: Element) => void): string {
return this.registerEvent('click', callback)
}
mount(el: HTMLElement): void {
for (const id in this.registry) {
const element = el.querySelector(`[data-id="${id}"]`)
if (element !== null) this.registry[id](element)
}
this.registry = {}
}
}

View File

@@ -1,5 +1,6 @@
import { DataModel, Path, ModelPath } from '@mcschema/core'
import { AbstractView } from './AbstractView'
import { transformOutput } from './hooks/transformOutput'
type SourceViewOptions = {
indentation?: number | string,
@@ -27,7 +28,7 @@ export class SourceView extends AbstractView {
}
invalidated() {
const transformed = this.model.schema.transform(new ModelPath(this.model), this.model.data)
const transformed = this.model.schema.hook(transformOutput, new ModelPath(this.model), this.model.data)
this.target.value = JSON.stringify(transformed, null, this.options?.indentation)
}

View File

@@ -1,5 +1,7 @@
import { DataModel, ModelPath, Mounter } from '@mcschema/core'
import { DataModel, ModelPath } from '@mcschema/core'
import { AbstractView } from './AbstractView'
import { Mounter } from './Mounter'
import { renderHtml } from './hooks/renderHtml'
type Registry = {
[id: string]: (el: Element) => void
@@ -39,7 +41,7 @@ export class TreeView extends AbstractView {
invalidated() {
const mounter = new Mounter({nodeInjector: this.nodeInjector})
const path = new ModelPath(this.model)
const rendered = this.model.schema.render(path, this.model.data, mounter)
const rendered = this.model.schema.hook(renderHtml, path, this.model.data, mounter)
const category = this.model.schema.category(path)
if (rendered[1]) {
this.target.innerHTML = `<div class="node ${this.model.schema.type(path)}-node" ${category ? `data-category="${category}"` : ''}>

View File

@@ -5,7 +5,6 @@ import {
locale,
LOCALES,
ModelPath,
Mounter,
Path,
} from '@mcschema/core'
import { getCollections, getSchemas } from '@mcschema/java-1.16'
@@ -16,6 +15,7 @@ import { SourceView } from './SourceView'
import { ErrorsView } from './ErrorsView'
import config from '../config.json'
import { BiomeNoiseVisualizer } from './visualization/BiomeNoiseVisualizer'
import { Mounter } from './Mounter'
const LOCAL_STORAGE_THEME = 'theme'
const LOCAL_STORAGE_LANGUAGE = 'language'

241
src/app/hooks/renderHtml.ts Normal file
View File

@@ -0,0 +1,241 @@
import { locale, Hook, ModelPath, Path, StringHookParams, ValidationOption, EnumOption, INode, DataModel, MapNode, StringNode } from '@mcschema/core'
import { Mounter } from '../Mounter'
import { hexId } from '../utils'
/**
* Secondary model used to remember the keys of a map
*/
const keysModel = new DataModel(MapNode(
StringNode(),
StringNode()
), { historyMax: 0 })
/**
* Renders the node and handles events to update the model
* @returns string HTML representation of this node using the given data
*/
export const renderHtml: Hook<[any, Mounter], [string, string, string]> = {
base() {
return ['', '', '']
},
boolean({ node }, path, value, mounter) {
const onFalse = mounter.registerClick(el => {
path.model.set(path, node.optional() && value === false ? undefined : false)
})
const onTrue = mounter.registerClick(el => {
path.model.set(path, node.optional() && value === true ? undefined : true)
})
return ['', `<button${value === false ? ' class="selected"' : ' '}
data-id="${onFalse}">${locale('false')}</button>
<button${value === true ? ' class="selected"' : ' '}
data-id="${onTrue}">${locale('true')}</button>`, '']
},
choice({ choices, config, switchNode }, path, value, mounter) {
const choice = switchNode.activeCase(path) ?? choices[0]
const pathWithContext = (config?.context) ?
new ModelPath(path.getModel(), new Path(path.getArray(), [config.context])) : path
const pathWithChoiceContext = config?.choiceContext ? new Path([], [config.choiceContext]) : config?.context ? new Path([], [config.context]) : path
const inject = choices.map(c => {
if (c.type === choice.type) {
return `<button class="selected" disabled>${pathWithChoiceContext.push(c.type).locale()}</button>`
}
const buttonId = mounter.registerClick(el => {
path.model.set(path, c.change ? c.change(value) : c.node.default())
})
return `<button data-id="${buttonId}">${pathWithChoiceContext.push(c.type).locale()}</button>`
}).join('')
const [prefix, suffix, body] = choice.node.hook(this, pathWithContext, value, mounter)
return [prefix, inject + suffix, body]
},
list({ children }, path, value, mounter) {
const onAdd = mounter.registerClick(el => {
if (!Array.isArray(value)) value = []
path.model.set(path, [children.default(), ...value])
})
const onAddBottom = mounter.registerClick(el => {
if (!Array.isArray(value)) value = []
path.model.set(path, [...value, children.default()])
})
const suffix = `<button class="add" data-id="${onAdd}"></button>`
+ mounter.nodeInjector(path, mounter)
let body = ''
if (Array.isArray(value)) {
body = value.map((childValue, index) => {
const removeId = mounter.registerClick(el => path.model.set(path.push(index), undefined))
const childPath = path.push(index).localePush('entry')
const category = children.category(childPath)
const [cPrefix, cSuffix, cBody] = children.hook(this, childPath, childValue, mounter)
return `<div class="node-entry"><div class="node ${children.type(childPath)}-node" ${category ? `data-category="${category}"` : ''} ${childPath.error()} ${childPath.help()}>
<div class="node-header">
<button class="remove" data-id="${removeId}"></button>
${cPrefix}
<label>${path.localePush('entry').locale([`${index}`])}</label>
${cSuffix}
</div>
${cBody ? `<div class="node-body">${cBody}</div>` : ''}
</div></div>`
}).join('')
if (value.length > 2) {
body += `<div class="node-entry">
<div class="node node-header">
<button class="add" data-id="${onAddBottom}"></button>
</div>
</div>`
}
}
return ['', suffix, body]
},
map({ keys, children }, path, value, mounter) {
const keyPath = new ModelPath(keysModel, new Path([hashString(path.toString())]))
const onAdd = mounter.registerClick(el => {
const key = keyPath.get()
path.model.set(path.push(key), children.default())
})
const keyRendered = keys.hook(this, keyPath, keyPath.get() ?? '', mounter)
const suffix = keyRendered[1]
+ `<button class="add" data-id="${onAdd}"></button>`
+ mounter.nodeInjector(path, mounter)
let body = ''
if (typeof value === 'object' && value !== undefined) {
body = Object.keys(value)
.map(key => {
const removeId = mounter.registerClick(el => path.model.set(path.push(key), undefined))
const childPath = path.modelPush(key)
const category = children.category(childPath)
const [cPrefix, cSuffix, cBody] = children.hook(this, childPath, value[key], mounter)
return `<div class="node-entry"><div class="node ${children.type(childPath)}-node" ${category ? `data-category="${category}"` : ''} ${childPath.error()} ${childPath.help()}>
<div class="node-header">
<button class="remove" data-id="${removeId}"></button>
${cPrefix}
<label>${key}</label>
${cSuffix}
</div>
${cBody ? `<div class="node-body">${cBody}</div>` : ''}
</div></div>`
})
.join('')
}
return ['', suffix, body]
},
number({ integer, config }, path, value, mounter) {
const onChange = mounter.registerChange(el => {
const value = (el as HTMLInputElement).value
let parsed = config?.color
? parseInt(value.slice(1), 16)
: integer ? parseInt(value) : parseFloat(value)
path.model.set(path, parsed)
})
if (config?.color) {
const hex = (value?.toString(16).padStart(6, '0') ?? '000000')
return ['', `<input type="color" data-id="${onChange}" value="#${hex}">`, '']
}
return ['', `<input data-id="${onChange}" value="${value ?? ''}">`, '']
},
object({ node, getActiveFields, getChildModelPath }, path, value, mounter) {
let prefix = ''
if (node.optional()) {
if (value === undefined) {
prefix = `<button class="collapse closed" data-id="${mounter.registerClick(() => path.model.set(path, node.default()))}"></button>`
} else {
prefix = `<button class="collapse open" data-id="${mounter.registerClick(() => path.model.set(path, undefined))}"></button>`
}
}
let suffix = mounter.nodeInjector(path, mounter)
let body = ''
if (typeof value === 'object' && value !== undefined && (!(node.optional() && value === undefined))) {
const activeFields = getActiveFields(path)
body = Object.keys(activeFields)
.filter(k => activeFields[k].enabled(path))
.map(k => {
const field = activeFields[k]
const childPath = getChildModelPath(path, k)
const category = field.category(childPath)
const [cPrefix, cSuffix, cBody] = field.hook(this, childPath, value[k], mounter)
return `<div class="node ${field.type(childPath)}-node" ${category ? `data-category="${category}"` : ''} ${childPath.error()} ${childPath.help()}>
<div class="node-header">
${cPrefix}
<label>${childPath.locale()}</label>
${cSuffix}
</div>
${cBody ? `<div class="node-body">${cBody}</div>` : ''}
</div>`
})
.join('')
}
return [prefix, suffix, body]
},
string(params, path, value, mounter) {
const inputId = mounter.register(el => {
(el as HTMLSelectElement).value = value ?? ''
el.addEventListener('change', evt => {
const newValue = (el as HTMLSelectElement).value
console.log("UPDATING NEW VALUE!", path.toString(), newValue)
path.model.set(path, newValue.length === 0 ? undefined : newValue)
evt.stopPropagation()
})
})
return ['', rawString(params, path, inputId), '']
}
}
function isEnum(value?: ValidationOption | EnumOption): value is EnumOption {
return !!(value as any)?.enum
}
function isValidator(value?: ValidationOption | EnumOption): value is ValidationOption {
return !!(value as any)?.validator
}
function rawString({ node, getValues, config }: { node: INode } & StringHookParams, path: ModelPath, inputId?: string) {
const values = getValues()
if (isEnum(config) && !config.additional) {
const contextPath = typeof config.enum === 'string' ?
new Path(path.getArray(), [config.enum]) : path
return selectRaw(node, contextPath, values, inputId)
}
if (config && isValidator(config)
&& config.validator === 'resource'
&& typeof config.params.pool === 'string'
&& values.length > 0) {
const contextPath = new Path(path.getArray(), [config.params.pool])
if (contextPath.localePush(values[0]).strictLocale()) {
return selectRaw(node, contextPath, values, inputId)
}
}
const datalistId = hexId()
return `<input data-id="${inputId}" ${values.length === 0 ? '' : `list="${datalistId}"`}>
${values.length === 0 ? '' :
`<datalist id="${datalistId}">
${values.map(v =>
`<option value="${v}">`
).join('')}
</datalist>`}`
}
function selectRaw(node: INode, contextPath: Path, values: string[], inputId?: string) {
return `<select data-id="${inputId}">
${node.optional() ? `<option value="">${locale('unset')}</option>` : ''}
${values.map(v =>
`<option value="${v}">${contextPath.localePush(v).locale()}</option>`
).join('')}
</select>`
}
function hashString(str: string) {
var hash = 0, i, chr;
for (i = 0; i < str.length; i++) {
chr = str.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0;
}
return hash;
}

View File

@@ -0,0 +1,53 @@
import { Hook } from '@mcschema/core'
export const transformOutput: Hook<[any], any> = {
base({}, _, value) {
return value
},
boolean({}, _, value) {
return value
},
choice({ switchNode }, path, value) {
return switchNode.hook(this, path, value)
},
list({ children }, path, value) {
if (!Array.isArray(value)) return value
return value.map((obj, index) =>
children.hook(this, path.push(index), obj)
)
},
map({ children }, path, value) {
if (value === undefined) return undefined
let res: any = {}
Object.keys(value).forEach(f =>
res[f] = children.hook(this, path.push(f), value[f])
)
return res;
},
number({}, _, value) {
return value
},
object({ getActiveFields }, path, value) {
if (value === undefined || value === null || typeof value !== 'object') {
return value
}
let res: any = {}
const activeFields = getActiveFields(path)
Object.keys(activeFields)
.filter(k => activeFields[k].enabled(path))
.forEach(f => {
res[f] = activeFields[f].hook(this, path.push(f), value[f])
})
return res
},
string({}, _, value) {
return value
}
}

7
src/app/utils.ts Normal file
View File

@@ -0,0 +1,7 @@
const dec2hex = (dec: number) => ('0' + dec.toString(16)).substr(-2)
export function hexId(length = 12) {
var arr = new Uint8Array(length / 2)
window.crypto.getRandomValues(arr)
return Array.from(arr, dec2hex).join('')
}