Move schemas to npm package

This commit is contained in:
Misode
2020-05-31 01:48:23 +02:00
parent e7fb692ac4
commit ea8e0e534e
24 changed files with 22 additions and 1996 deletions

View File

@@ -13,6 +13,7 @@
"license": "MIT",
"dependencies": {
"copy-webpack-plugin": "^6.0.1",
"minecraft-schemas": "^0.1.2",
"ts-loader": "^7.0.4",
"typescript": "^3.9.3",
"webpack": "^4.43.0",

View File

@@ -1,103 +0,0 @@
import { INode } from "./nodes/AbstractNode"
import { Path, PathElement } from "./model/Path"
export interface Registry<T> {
register(id: string, value: T): void
get(id: string): T
}
/**
* Registry for schemas
*/
class SchemaRegistry implements Registry<INode<any>> {
private registry: { [id: string]: INode<any> } = {}
register(id: string, node: INode<any>) {
this.registry[id] = node
}
get(id: string) {
const node = this.registry[id]
if (node === undefined) {
console.error(`Tried to access schema "${id}, but that doesn't exit.`)
}
return node
}
}
/**
* Registry for collections
*/
class CollectionRegistry implements Registry<string[]> {
private registry: { [id: string]: string[] } = {}
register(id: string, list: string[]) {
this.registry[id] = list
}
get(id: string) {
const list = this.registry[id]
if (list === undefined) {
console.warn(`Tried to access collection "${id}", but that doesn't exist`)
}
return list ?? []
}
}
/**
* Registry for locales
*/
export interface Locale {
[key: string]: string
}
class LocaleRegistry implements Registry<Locale> {
private registry: { [id: string]: Locale } = {}
language: string = ''
/**
*
* @param id locale identifier
* @param locale object mapping keys to translations
*/
register(id: string, locale: Locale): void {
this.registry[id] = locale
}
get(id: string) {
const locale = this.registry[id]
if (locale === undefined) {
console.warn(`Tried to access locale "${id}", but that doesn't exist`)
}
return locale ?? []
}
getLocale(key: string) {
return this.get(this.language)[key]
}
}
export const SCHEMAS = new SchemaRegistry()
export const COLLECTIONS = new CollectionRegistry()
export const LOCALES = new LocaleRegistry()
/**
* Gets the locale of a key from the locale registry.
*
* @param key string or path that refers to a locale ID.
* If a string is given, an exact match is required.
* If a path is given, it finds the longest match at the end.
* @returns undefined if the key isn't found for the selected language
*/
export const locale = (key: string | Path) => {
if (typeof key === 'string') {
return LOCALES.getLocale(key) ?? key
}
let path = key.getArray().filter(e => (typeof e === 'string'))
while (path.length > 0) {
const locale = LOCALES.getLocale(path.join('.'))
if (locale !== undefined) return locale
path.shift()
}
return key.last()
}

View File

@@ -1,11 +1,14 @@
import { ObjectNode } from '../nodes/ObjectNode';
import { EnumNode } from '../nodes/EnumNode';
import { NumberNode } from '../nodes/NumberNode';
import { BooleanNode } from '../nodes/BooleanNode';
import { RangeNode } from '../minecraft/nodes/RangeNode';
import { MapNode } from '../nodes/MapNode';
import { StringNode } from '../nodes/StringNode';
import { ListNode } from '../nodes/ListNode';
import {
ObjectNode,
EnumNode,
StringNode,
NumberNode,
BooleanNode,
RangeNode,
MapNode,
ListNode,
SCHEMAS
} from 'minecraft-schemas'
const EntityCollection = ['sheep', 'pig']

View File

@@ -1,10 +1,13 @@
import { DataModel } from '../model/DataModel'
import { TreeView } from '../view/TreeView'
import { SourceView } from '../view/SourceView'
import { ConditionSchema } from '../minecraft/schemas/Condition'
import { LootTableSchema } from '../minecraft/schemas/LootTable'
import {
DataModel,
TreeView,
SourceView,
ConditionSchema,
LootTableSchema,
LOCALES
} from 'minecraft-schemas'
import { SandboxSchema } from './Sandbox'
import { LOCALES } from '../Registries'
const predicateModel = new DataModel(ConditionSchema)
const lootTableModel = new DataModel(LootTableSchema)

View File

@@ -1,106 +0,0 @@
import { AbstractNode, NodeMods, RenderOptions, StateNode } from '../../nodes/AbstractNode'
import { Path } from '../../model/Path'
import { DataModel } from '../../model/DataModel'
import { TreeView } from '../../view/TreeView'
import { locale } from '../../Registries'
export type IRange = number
| { min?: number, max?: number, type?: 'uniform' }
| { n?: number, p?: number, type: 'binomial' }
export interface RangeNodeMods extends NodeMods<IRange> {
integer?: boolean
}
export class RangeNode extends AbstractNode<IRange> implements StateNode<IRange> {
integer: boolean
constructor(mods?: RangeNodeMods) {
super(mods)
this.integer = mods?.integer ? mods.integer : false
}
parseNumber(str: string): number {
return this.integer ? parseInt(str) : parseFloat(str)
}
getState(el: Element): IRange {
const type = el.querySelector('select')!.value
if (type === 'exact') {
return this.parseNumber(el.querySelector('input')!.value)
}
if (type === 'range') {
const min = this.parseNumber(el.querySelectorAll('input')[0].value)
const max = this.parseNumber(el.querySelectorAll('input')[1].value)
return {
min: isNaN(min) ? undefined : min,
max: isNaN(max) ? undefined : max
}
}
const n = parseInt(el.querySelectorAll('input')[0].value)
const p = parseFloat(el.querySelectorAll('input')[1].value)
return {
type: 'binomial',
n: isNaN(n) ? undefined : n,
p: isNaN(p) ? undefined : p
}
}
updateModel(el: Element, path: Path, model: DataModel) {
model.set(path, this.getState(el))
}
renderRaw(path: Path, value: IRange, view: TreeView, options?: RenderOptions) {
let curType = ''
let input = ''
if (value === undefined || typeof value === 'number') {
curType = 'exact'
input = `<input value=${value === undefined ? '' : value}>`
} else if (value.type === 'binomial') {
curType = 'binomial'
input = `<label>${locale('range.n')}</label>
<input value=${value.n === undefined ? '' : value.n}>
<label>${locale('range.p')}</label>
<input value=${value.p === undefined ? '' : value.p}>`
} else {
curType = 'range'
input = `<label>${locale('range.min')}</label>
<input value=${value.min === undefined ? '' : value.min}>
<label>${locale('range.max')}</label>
<input value=${value.max === undefined ? '' : value.max}>`
}
const id = view.register(el => {
(el as HTMLInputElement).value = curType
el.addEventListener('change', evt => {
const target = (el as HTMLInputElement).value
const newValue = this.default(target === 'exact' ? undefined :
target === 'binomial' ? {type: 'binomial'} : {})
view.model.set(path, newValue)
evt.stopPropagation()
})
})
return `${options?.hideLabel ? `` : `<label>${locale(path)}</label>`}
<select data-id="${id}">
<option value="exact">${locale('range.exact')}</option>
<option value="range">${locale('range.range')}</option>
<option value="binomial">${locale('range.binomial')}</option>
</select>
${input}`
}
getClassName() {
return 'range-node'
}
static isExact(v?: IRange) {
return v === undefined || typeof v === 'number'
}
static isRange(v?: IRange) {
return v !== undefined && typeof v !== 'number' && v.type !== 'binomial'
}
static isBinomial(v?: IRange) {
return v !== undefined && typeof v !== 'number' && v.type === 'binomial'
}
}

View File

@@ -1,47 +0,0 @@
import { NodeMods, RenderOptions } from '../../nodes/AbstractNode'
import { EnumNode } from '../../nodes/EnumNode'
import { Path } from '../../model/Path'
import { TreeView, getId } from '../../view/TreeView'
import { locale } from '../../Registries'
export interface ResourceNodeMods extends NodeMods<string> {
additional?: boolean
}
export class ResourceNode extends EnumNode {
additional: boolean
constructor(options: string[], mods?: ResourceNodeMods) {
super(options, {
transform: (v) => {
if (v === undefined || v.length === 0) return undefined
return v.startsWith('minecraft:') ? v : 'minecraft:' + v
}, ...mods})
this.additional = mods?.additional ?? false
}
getState(el: Element) {
if (this.additional) {
return el.querySelector('input')!.value
} else {
return super.getState(el)
}
}
renderRaw(path: Path, value: string, view: TreeView, options?: RenderOptions) {
if (this.additional) {
const id = `datalist-${getId()}`
return `${options?.hideLabel ? `` : `<label>${locale(path)}</label>`}
<input list=${id} value="${value ?? ''}">
<datalist id=${id}>${this.options.map(o =>
`<option value="${o}">${locale(path.push(o))}</option>`
).join('')}</datalist>`
} else {
return super.renderRaw(path, value, view, options)
}
}
getClassName() {
return 'enum-node'
}
}

View File

@@ -1,321 +0,0 @@
import { COLLECTIONS } from '../../Registries'
COLLECTIONS.register('conditions', [
'alternative',
'requirements',
'inverted',
'reference',
'entity_properties',
'block_state_property',
'match_tool',
'damage_source_properties',
'location_check',
'weather_check',
'time_check',
'entity_scores',
'random_chance',
'random_chance_with_looting',
'table_bonus',
'killed_by_player',
'survives_explosion'
])
COLLECTIONS.register('loot-entries', [
'empty',
'item',
'tag',
'loot_table',
'alternatives',
'sequence',
'group',
'dynamic'
])
COLLECTIONS.register('loot-functions', [
'set_count',
'set_damage',
'set_name',
'set_lore',
'set_nbt',
'set_attributes',
'set_contents',
'enchant_randomly',
'enchant_with_levels',
'looting_enchant',
'limit_count',
'furnace_smelt',
'explosion_decay',
'fill_player_head',
'copy_name',
'copy_nbt',
'copy_state',
'apply_bonus',
'exploration_map',
'set_stew_effect'
])
COLLECTIONS.register('enchantments', [
'aqua_affinity',
'bane_of_arthropods',
'blast_protection',
'channeling',
'binding_curse',
'vanishing_curse',
'depth_strider',
'efficiency',
'feather_falling',
'fire_aspect',
'fire_protection',
'flame',
'fortune',
'frost_walker',
'impaling',
'infinity',
'knockback',
'looting',
'loyalty',
'luck_of_the_sea',
'lure',
'mending',
'multishot',
'piercing',
'power',
'projectile_protection',
'protection',
'punch',
'quick_charge',
'respiration',
'riptide',
'sharpness',
'silk_touch',
'smite',
'sweeping',
'thorns',
'unbreaking'
])
COLLECTIONS.register('biomes', [
'badlands',
'badlands_plateau',
'bamboo_jungle',
'bamboo_jungle_hills',
'beach',
'birch_forest',
'birch_forest_hills',
'cold_ocean',
'dark_forest',
'dark_forest_hills',
'deep_cold_ocean',
'deep_frozen_ocean',
'deep_lukewarm_ocean',
'deep_ocean',
'deep_warm_ocean',
'desert',
'desert_hills',
'desert_lakes',
'end_barrens',
'end_highlands',
'end_midlands',
'eroded_badlands',
'flower_forest',
'forest',
'frozen_ocean',
'frozen_river',
'giant_spruce_taiga',
'giant_spruce_taiga_hills',
'giant_tree_taiga',
'giant_tree_taiga_hills',
'gravelly_mountains',
'ice_spikes',
'jungle',
'jungle_edge',
'jungle_hills',
'lukewarm_ocean',
'modified_badlands_plateau',
'modified_gravelly_mountains',
'modified_jungle',
'modified_jungle_edge',
'modified_wooded_badlands_plateau',
'mountain_edge',
'mountains',
'mushroom_field_shore',
'mushroom_fields',
'nether',
'ocean',
'plains',
'river',
'savanna',
'savanna_plateau',
'shattered_savanna',
'shattered_savanna_plateau',
'small_end_islands',
'snowy_beach',
'snowy_mountains',
'snowy_taiga',
'snowy_taiga_hills',
'snowy_taiga_mountains',
'snowy_tundra',
'stone_shore',
'sunflower_plains',
'swamp',
'swamp_hills',
'taiga',
'taiga_hills',
'taiga_mountains',
'tall_birch_forest',
'tall_birch_hills',
'the_end',
'the_void',
'warm_ocean',
'wooded_badlands_plateau',
'wooded_hills',
'wooded_mountains'
])
COLLECTIONS.register('structures', [
'pillager_outpost',
'mineshaft',
'mansion',
'jungle_pyramid',
'desert_pyramid',
'igloo',
'shipwreck',
'swamp_hut',
'stronghold',
'monument',
'ocean_ruin',
'fortress',
'endcity',
'buried_treasure',
'village'
])
COLLECTIONS.register('dimensions', [
'overworld',
'the_nether',
'the_end'
])
COLLECTIONS.register('slots', [
'mainhand',
'offhand',
'head',
'chest',
'legs',
'feet'
])
COLLECTIONS.register('status-effects', [
'speed',
'slowness',
'haste',
'mining_fatigue',
'strength',
'instant_health',
'instant_damage',
'jump_boost',
'nausea',
'regeneration',
'resistance',
'fire_resistance',
'water_breathing',
'invisibility',
'blindness',
'night_vision',
'hunger',
'weakness',
'poison',
'wither',
'health_boost',
'absorption',
'saturation',
'glowing',
'levitation',
'luck',
'unluck',
'slow_falling',
'conduit_power',
'dolphins_grace',
'bad_omen',
'hero_of_the_village'
])
COLLECTIONS.register('gamemodes', [
'survival',
'creative',
'adventure',
'spectator'
])
COLLECTIONS.register('statistic-types', [
'minecraft:broken',
'minecraft:crafted',
'minecraft:custom',
'minecraft:dropped',
'minecraft:killed',
'minecraft:killed_by',
'minecraft:mined',
'minecraft:picked_up',
'minecraft:used',
'killedByTeam',
'teamkill'
])
COLLECTIONS.register('entity-sources', [
'this',
'killer',
'killer_player'
])
COLLECTIONS.register('copy-sources', [
'block_entity',
'this',
'killer',
'killer_player'
])
COLLECTIONS.register('attributes', [
'generic.max_health',
'generic.follow_range',
'generic.knockback_resistance',
'generic.movement_speed',
'generic.attack_damage',
'generic.armor',
'generic.armor_toughness',
'generic.attack_speed',
'generic.luck',
'horse.jump_strength',
'generic.attack_knockback',
'generic.flying_speed',
'zombie.spawn_reinforcements'
])
COLLECTIONS.register('map-decorations', [
'mansion',
'monument',
'player',
'frame',
'red_marker',
'blue_marker',
'target_x',
'target_point',
'player_off_map',
'player_off_limits',
'red_x',
'banner_white',
'banner_orange',
'banner_magenta',
'banner_light_blue',
'banner_yellow',
'banner_lime',
'banner_pink',
'banner_gray',
'banner_light_gray',
'banner_cyan',
'banner_purple',
'banner_blue',
'banner_brown',
'banner_green',
'banner_red',
'banner_black'
])

View File

@@ -1,97 +0,0 @@
import { EnumNode } from '../../nodes/EnumNode';
import { ResourceNode } from '../nodes/ResourceNode';
import { NumberNode } from '../../nodes/NumberNode';
import { BooleanNode } from '../../nodes/BooleanNode';
import { ObjectNode, Switch, Case } from '../../nodes/ObjectNode';
import { ListNode } from '../../nodes/ListNode';
import { RangeNode } from '../nodes/RangeNode';
import { MapNode } from '../../nodes/MapNode';
import { StringNode } from '../../nodes/StringNode';
import { ReferenceNode } from '../../nodes/ReferenceNode';
import { SCHEMAS, COLLECTIONS } from '../../Registries';
import './Predicates'
SCHEMAS.register('condition', new ObjectNode({
condition: new ResourceNode(COLLECTIONS.get('conditions'), {default: () => 'random_chance'}),
[Switch]: 'condition',
[Case]: {
'alternative': {
terms: new ListNode(
new ReferenceNode('condition')
)
},
'block_state_property': {
block: new ResourceNode(COLLECTIONS.get('blocks')),
properties: new MapNode(
new StringNode(),
new StringNode()
)
},
'damage_source_properties': {
predicate: new ReferenceNode('damage-source-predicate')
},
'entity_properties': {
entity: new EnumNode(COLLECTIONS.get('entity-sources'), 'this'),
predicate: new ReferenceNode('entity-predicate')
},
'entity_scores': {
entity: new EnumNode(COLLECTIONS.get('entity-sources'), 'this'),
scores: new MapNode(
new StringNode(),
new RangeNode()
)
},
'inverted': {
term: new ReferenceNode('condition')
},
'killed_by_player': {
inverse: new BooleanNode()
},
'location_check': {
offsetX: new NumberNode({integer: true}),
offsetY: new NumberNode({integer: true}),
offsetZ: new NumberNode({integer: true}),
predicate: new ReferenceNode('location-predicate')
},
'match_tool': {
predicate: new ReferenceNode('item-predicate')
},
'random_chance': {
chance: new NumberNode({min: 0, max: 1})
},
'random_chance_with_looting': {
chance: new NumberNode({min: 0, max: 1}),
looting_multiplier: new NumberNode()
},
'requirements': {
terms: new ListNode(
new ReferenceNode('condition')
),
},
'reference': {
name: new StringNode()
},
'table_bonus': {
enchantment: new ResourceNode(COLLECTIONS.get('enchantments')),
chances: new ListNode(
new NumberNode({min: 0, max: 1})
)
},
'time_check': {
value: new RangeNode(),
period: new NumberNode()
},
'weather_check': {
raining: new BooleanNode(),
thrundering: new BooleanNode()
}
}
}, {
default: () => ({
condition: 'random_chance',
chance: 0.5
})
}))
export const ConditionSchema = SCHEMAS.get('condition')

View File

@@ -1,247 +0,0 @@
import { EnumNode } from '../../nodes/EnumNode';
import { ResourceNode } from '../nodes/ResourceNode';
import { NumberNode } from '../../nodes/NumberNode';
import { BooleanNode } from '../../nodes/BooleanNode';
import { ObjectNode, Switch, Case } from '../../nodes/ObjectNode';
import { ListNode } from '../../nodes/ListNode';
import { RangeNode } from '../nodes/RangeNode';
import { MapNode } from '../../nodes/MapNode';
import { StringNode } from '../../nodes/StringNode';
import { ReferenceNode } from '../../nodes/ReferenceNode';
import { SCHEMAS, COLLECTIONS } from '../../Registries';
import './Predicates'
const conditions = {
conditions: new ListNode(
new ReferenceNode('condition')
)
}
const functionsAndConditions = {
functions: new ListNode(
new ReferenceNode('loot-function')
),
...conditions
}
SCHEMAS.register('loot-table', new ObjectNode({
pools: new ListNode(
new ObjectNode({
rolls: new RangeNode(),
entries: new ListNode(
new ReferenceNode('loot-entry')
)
})
),
...functionsAndConditions
}, {
default: () => ({
pools: [
{
rolls: 1,
entries: [
{
type: 'item',
name: 'minecraft:stone'
}
]
}
]
})
}))
SCHEMAS.register('loot-entry', new ObjectNode({
type: new EnumNode(COLLECTIONS.get('loot-entries'), {default: () => 'item'}),
weight: new NumberNode({
integer: true,
min: 1,
enable: path => path.pop().get()?.length > 1
&& !['alternatives', 'group', 'sequence'].includes(path.push('type').get())
}),
[Switch]: 'type',
[Case]: {
'alternatives': {
children: new ListNode(
new ReferenceNode('loot-entry')
),
...functionsAndConditions
},
'dynamic': {
name: new StringNode(),
...functionsAndConditions
},
'group': {
children: new ListNode(
new ReferenceNode('loot-entry')
),
...functionsAndConditions
},
'item': {
name: new StringNode(),
...functionsAndConditions
},
'loot_table': {
name: new StringNode(),
...functionsAndConditions
},
'sequence': {
children: new ListNode(
new ReferenceNode('loot-entry')
),
...functionsAndConditions
},
'tag': {
name: new StringNode(),
expand: new BooleanNode(),
...functionsAndConditions
}
}
}))
SCHEMAS.register('loot-function', new ObjectNode({
function: new EnumNode(COLLECTIONS.get('loot-functions'), {default: () => 'set_count'}),
[Switch]: 'function',
[Case]: {
'apply_bonus': {
enchantment: new EnumNode(COLLECTIONS.get('enchantments')),
formula: new EnumNode([
'uniform_bonus_count',
'binomial_with_bonus_count',
'ore_drops'
]),
parameters: new ObjectNode({
bonusMultiplier: new NumberNode({
enable: path => path.pop().push('formula').get() === 'uniform_bonus_count'
}),
extra: new NumberNode({
enable: path => path.pop().push('formula').get() === 'binomial_with_bonus_count'
}),
probability: new NumberNode({
enable: path => path.pop().push('formula').get() === 'binomial_with_bonus_count'
})
}, {
enable: path => path.push('formula').get() !== 'ore_drops'
}),
...conditions
},
'copy_name': {
source: new EnumNode(COLLECTIONS.get('copy-sources')),
...conditions
},
'copy_nbt': {
source: new EnumNode(COLLECTIONS.get('copy-sources')),
ops: new ListNode(
new ObjectNode({
source: new StringNode(),
target: new StringNode(),
op: new EnumNode(['replace', 'append', 'merge'])
})
),
...conditions
},
'copy_state': {
block: new ResourceNode(COLLECTIONS.get('blocks')),
properties: new ListNode(
new StringNode()
),
...conditions
},
'enchant_randomly': {
enchantments: new ListNode(
new EnumNode(COLLECTIONS.get('enchantments'))
),
...conditions
},
'enchant_with_levels': {
levels: new RangeNode(),
treasure: new BooleanNode(),
...conditions
},
'exploration_map': {
destination: new EnumNode(COLLECTIONS.get('structures')),
decoration: new EnumNode(COLLECTIONS.get('map-decorations')),
zoom: new NumberNode({integer: true}),
search_radius: new NumberNode({integer: true}),
skip_existing_chunks: new BooleanNode(),
...conditions
},
'fill_player_head': {
entity: new EnumNode(COLLECTIONS.get('entity-sources')),
...conditions
},
'limit_count': {
limit: new RangeNode(),
...conditions
},
'looting_enchant': {
count: new RangeNode(),
limit: new NumberNode({integer: true}),
...conditions
},
'set_attributes': {
modifiers: new ListNode(
new ReferenceNode('attribute-modifier')
),
...conditions
},
'set_contents': {
entries: new ListNode(
new ReferenceNode('loot-entry')
),
...conditions
},
'set_count': {
count: new RangeNode(),
...conditions
},
'set_damage': {
damage: new RangeNode(),
...conditions
},
'set_lore': {
entity: new EnumNode(COLLECTIONS.get('entity-sources')),
lore: new ListNode(
new StringNode()
),
replace: new BooleanNode(),
...conditions
},
'set_name': {
entity: new EnumNode(COLLECTIONS.get('entity-sources')),
name: new StringNode(),
...conditions
},
'set_nbt': {
tag: new StringNode(),
...conditions
},
'set_stew_effect': {
effects: new ListNode(
new ReferenceNode('potion-effect')
),
...conditions
}
}
}, {
default: () => ({
function: 'set_count',
count: 1
})
}))
SCHEMAS.register('attribute-modifier', new ObjectNode({
attribute: new EnumNode(COLLECTIONS.get('attributes')),
name: new StringNode(),
amount: new RangeNode(),
operation: new EnumNode([
'addition',
'multiply_base',
'multiply_total'
]),
slot: new ListNode(
new EnumNode(COLLECTIONS.get('slots'))
)
}))
export const LootTableSchema = SCHEMAS.get('loot-table')

View File

@@ -1,132 +0,0 @@
import { ObjectNode } from '../../nodes/ObjectNode';
import { ResourceNode } from '../nodes/ResourceNode';
import { EnumNode } from '../../nodes/EnumNode';
import { ListNode } from '../../nodes/ListNode';
import { RangeNode } from '../nodes/RangeNode';
import { StringNode } from '../../nodes/StringNode';
import { ReferenceNode } from '../../nodes/ReferenceNode';
import { BooleanNode } from '../../nodes/BooleanNode';
import { MapNode } from '../../nodes/MapNode';
import { SCHEMAS, COLLECTIONS } from '../../Registries';
import './Collections'
SCHEMAS.register('item-predicate', new ObjectNode({
item: new ResourceNode(COLLECTIONS.get('items')),
tag: new StringNode(),
count: new RangeNode(),
durability: new RangeNode(),
potion: new StringNode(),
nbt: new StringNode(),
enchantments: new ListNode(
new ReferenceNode('enchantment-predicate')
)
}))
SCHEMAS.register('enchantment-predicate', new ObjectNode({
enchantment: new ResourceNode(COLLECTIONS.get('enchantments')),
levels: new RangeNode()
}))
SCHEMAS.register('block-predicate', new ObjectNode({
block: new ResourceNode(COLLECTIONS.get('blocks')),
tag: new StringNode(),
nbt: new StringNode(),
state: new MapNode(
new StringNode(),
new StringNode()
)
}))
SCHEMAS.register('fluid-predicate', new ObjectNode({
fluid: new ResourceNode(COLLECTIONS.get('fluids')),
tag: new StringNode(),
nbt: new StringNode(),
state: new MapNode(
new StringNode(),
new StringNode()
)
}))
SCHEMAS.register('location-predicate', new ObjectNode({
position: 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}),
light: new ObjectNode({
light: new RangeNode()
}),
smokey: new BooleanNode(),
block: new ReferenceNode('block-predicate', {collapse: true}),
fluid: new ReferenceNode('fluid-predicate', {collapse: true})
}))
SCHEMAS.register('statistic-predicate', new ObjectNode({
type: new EnumNode(COLLECTIONS.get('statistic-types')),
stat: new StringNode(),
value: new RangeNode()
}))
SCHEMAS.register('player-predicate', new ObjectNode({
gamemode: new EnumNode(COLLECTIONS.get('gamemodes')),
level: new RangeNode(),
advancements: new MapNode(
new StringNode(),
new BooleanNode()
),
recipes: new MapNode(
new StringNode(),
new BooleanNode()
),
stats: new ListNode(
new ReferenceNode('statistic-predicate')
)
}))
SCHEMAS.register('status-effect-predicate', new ObjectNode({
amplifier: new RangeNode(),
duration: new RangeNode(),
ambient: new BooleanNode(),
visible: new BooleanNode()
}))
SCHEMAS.register('distance-predicate', new ObjectNode({
x: new RangeNode(),
y: new RangeNode(),
z: new RangeNode(),
absolute: new RangeNode(),
horizontal: new RangeNode()
}))
SCHEMAS.register('entity-predicate', new ObjectNode({
type: new StringNode(),
nbt: new StringNode(),
team: new StringNode(),
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', {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()
}),
effects: new MapNode(
new ResourceNode(COLLECTIONS.get('status-effects')),
new ReferenceNode('status-effect-predicate')
)
}))

View File

@@ -1,91 +0,0 @@
import { Path } from "./Path"
import { INode } from "../nodes/AbstractNode"
export interface ModelListener {
invalidated(model: DataModel): void
}
/**
* Holding the data linked to a given schema
*/
export class DataModel {
data: any
schema: INode<any>
/** A list of listeners that want to be notified when the model is invalidated */
listeners: ModelListener[]
/**
* @param schema node to use as schema for this model
*/
constructor(schema: INode<any>) {
this.schema = schema
this.data = schema.default()
this.listeners = []
}
/**
* @param listener the listener to notify when the model is invalidated
*/
addListener(listener: ModelListener) {
this.listeners.push(listener)
}
/**
* Force notify all listeners that the model is invalidated
*/
invalidate() {
this.listeners.forEach(listener => listener.invalidated(this))
}
/**
* Resets the full data and notifies listeners
* @param value new model data
*/
reset(value: any) {
this.data = value
this.invalidate()
}
/**
* Gets the data at a specified path
* @param path path at which to find the data
* @returns undefined, if the the path does not exist in the data
*/
get(path: Path) {
let node = this.data;
for (let index of path) {
if (node === undefined) return node
node = node[index]
}
return node
}
/**
* Updates the date on a path. Node will be removed when value is undefined
* @param path path to update
* @param value new data at the specified path
*/
set(path: Path, value: any) {
let node = this.data;
for (let index of path.pop()) {
if (node[index] === undefined) {
node[index] = {}
}
node = node[index]
}
console.log('Set', path.toString(), JSON.stringify(value))
if (value === undefined || (typeof value === 'number' && isNaN(value))) {
if (typeof path.last() === 'number') {
node.splice(path.last(), 1)
} else {
delete node[path.last()]
}
} else {
node[path.last()] = value
}
this.invalidate()
}
}

View File

@@ -1,77 +0,0 @@
import { DataModel } from "./DataModel"
export type PathElement = (string | number)
/**
* Immutable helper class to represent a path in data
* @implements {Iterable<PathElement>}
*/
export class Path implements Iterable<PathElement> {
private arr: PathElement[]
model?: DataModel
/**
* @param arr Initial array of path elements. Empty if not given
* @param model Model attached to this path
*/
constructor(arr?: PathElement[], model?: DataModel) {
this.arr = arr ?? []
this.model = model
}
/**
* The last element of this path
*/
last(): PathElement {
return this.arr[this.arr.length - 1]
}
/**
* A new path with the last element removed
*/
pop(): Path {
return new Path(this.arr.slice(0, -1), this.model)
}
/**
* A new path with an element added at the end
* @param element element to push at the end of the array
*/
push(element: PathElement): Path {
return new Path([...this.arr, element], this.model)
}
copy(): Path {
return new Path([...this.arr], this.model)
}
getArray(): PathElement[] {
return this.arr
}
/**
* Attaches a model to this path and all paths created from this
* @param model
*/
withModel(model: DataModel): Path {
return new Path([...this.arr], model)
}
/**
* Gets the data from the model if it was attached
* @returns undefined, if no model was attached
*/
get(): any {
return this.model?.get(this)
}
toString(): string {
return `[${this.arr.map(e => e.toString()).join(', ')}]`
}
*[Symbol.iterator]() {
for (const e of this.arr) {
yield e
}
}
}

View File

@@ -1,149 +0,0 @@
import { DataModel } from "../model/DataModel"
import { Path } from "../model/Path"
import { TreeView } from "../view/TreeView"
/**
* Schema node that supports some standard transformations
*/
export interface INode<T> {
default: (value?: T) => T | undefined
transform: (path: Path, value: T) => any
enabled: (path: Path, model: DataModel) => boolean
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
collapse?: boolean
}
export type NodeChildren = {
[name: string]: INode<any>
}
export type IDefault<T> = (value?: T) => T | undefined
export type ITransform<T> = (value: T) => any
export type IEnable = (path: Path) => boolean
export type IForce = () => boolean
export interface NodeMods<T> {
default?: IDefault<T>
transform?: ITransform<T>
enable?: IEnable
force?: IForce
}
/**
* Basic implementation of the nodes
*
* h
*/
export abstract class AbstractNode<T> implements INode<T> {
defaultMod: IDefault<T>
transformMod: ITransform<T>
enableMod: IEnable
forceMod: IForce
/**
* @param mods modifiers of the default transformations
*/
constructor(mods?: NodeMods<T>) {
this.defaultMod = mods?.default ?? ((v) => v)
this.transformMod = mods?.transform ?? ((v) => v)
this.enableMod = mods?.enable ?? (() => true)
this.forceMod = mods?.force ?? (() => false)
}
/**
* Runs when the element is mounted to the DOM
* @param el mounted element
* @param path data path that this node represents
* @param view corresponding tree view where this was mounted
*/
mounted(el: Element, path: Path, view: TreeView) {
el.addEventListener('change', evt => {
this.updateModel(el, path, view.model)
evt.stopPropagation()
})
}
/**
* Runs when the DOM element 'change' event is called
* @param el mounted element
* @param path data path that this node represents
* @param model corresponding model
*/
updateModel(el: Element, path: Path, model: DataModel) {}
/**
* The default value of this node
* @param value optional original value
*/
default(value?: T) {
return this.defaultMod(value)
}
/**
* Transforms the data model to the final output format
* @param
*/
transform(path: Path, value: T) {
if (!this.enabled(path)) return undefined
if (value === undefined && this.force()) value = this.default(value)!
return this.transformMod(value)
}
/**
* Determines whether the node should be enabled for this path
* @param path
* @param model
*/
enabled(path: Path, model?: DataModel) {
if (model) path = path.withModel(model)
return this.enableMod(path.pop())
}
force(): boolean {
return this.forceMod()
}
/**
* Wraps and renders the node
* @param path location
* @param value data used at
* @param view tree view context, containing the model
* @param options optional render options
* @returns string HTML wrapped representation of this node using the given data
*/
render(path: Path, value: T, view: TreeView, options?: RenderOptions): string {
if (!this.enabled(path, view.model)) return ''
const id = view.register(el => {
this.mounted(el, path, view)
})
return `<div data-id="${id}" class="node ${this.getClassName()}">
${this.renderRaw(path, value, view, options)}
</div>`
}
/**
* Renders the node and handles events to update the model
* @param path
* @param value
* @param view tree view context, containing the model
* @param options optional render options
* @returns string HTML representation of this node using the given data
*/
abstract renderRaw(path: Path, value: T, view: TreeView, options?: RenderOptions): string
/**
* The CSS classname used in the wrapped <div>
*/
abstract getClassName(): string
}

View File

@@ -1,37 +0,0 @@
import { AbstractNode, NodeMods, RenderOptions } from "./AbstractNode";
import { Path } from "../model/Path";
import { TreeView } from "../view/TreeView";
import { locale } from "../Registries";
/**
* Boolean node with two buttons for true/false
*/
export class BooleanNode extends AbstractNode<boolean> {
/**
* @param mods optional node modifiers
*/
constructor(mods?: NodeMods<boolean>) {
super({
default: () => false,
...mods})
}
renderRaw(path: Path, value: boolean, view: TreeView, options?: RenderOptions) {
const falseButton = view.registerClick(el => {
view.model.set(path, !this.force() && value === false ? undefined : false)
})
const trueButton = view.registerClick(el => {
view.model.set(path, !this.force() && value === true ? undefined : true)
})
return `${options?.hideLabel ? `` : `<label>${locale(path)}</label>`}
<button${value === false ? ' style="font-weight: bold"' : ' '}
data-id="${falseButton}">${locale('false')}</button>
<button${value === true ? ' style="font-weight: bold"' : ' '}
data-id="${trueButton}">${locale('true')}</button>`
}
getClassName() {
return 'boolean-node'
}
}

View File

@@ -1,47 +0,0 @@
import { AbstractNode, NodeMods, RenderOptions, StateNode } from './AbstractNode'
import { DataModel } from '../model/DataModel'
import { TreeView } from '../view/TreeView'
import { Path } from '../model/Path'
import { locale } from '../Registries'
/**
* Enum node that shows a list of options to choose from
*/
export class EnumNode extends AbstractNode<string> implements StateNode<string> {
protected options: string[]
static className = 'enum-node'
/**
* @param options options to choose from in the select
* @param mods optional node modifiers or a string to be the default value
*/
constructor(options: string[], mods?: NodeMods<string> | string) {
super(typeof mods === 'string' ? {
default: () => mods,
force: () => true
} : mods)
this.options = options
}
getState(el: Element) {
return el.querySelector('select')!.value
}
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 `${options?.hideLabel ? `` : `<label>${locale(path)}</label>`}
<select data-id=${id}>
${this.options.map(o =>
`<option value="${o}">${locale(path.push(o))}</option>`
).join('')}
</select>`
}
getClassName() {
return 'enum-node'
}
}

View File

@@ -1,63 +0,0 @@
import { AbstractNode, NodeMods, INode } from './AbstractNode'
import { DataModel } from '../model/DataModel'
import { TreeView } from '../view/TreeView'
import { Path } from '../model/Path'
import { IObject } from './ObjectNode'
import { locale } from '../Registries'
/**
* List node where children can be added and removed from
*/
export class ListNode extends AbstractNode<IObject[]> {
protected children: INode<any>
/**
* @param values node used for its children
* @param mods optional node modifiers
*/
constructor(values: INode<any>, mods?: NodeMods<IObject[]>) {
super({
default: () => [],
...mods})
this.children = values
}
transform(path: Path, value: IObject[]) {
if (!(value instanceof Array)) return undefined
const res = value.map((obj, index) =>
this.children.transform(path.push(index), obj)
)
return this.transformMod(res)
}
updateModel(el: Element, path: Path, model: DataModel) {
model.set(path, el.querySelector('select')?.value)
}
renderRaw(path: Path, value: IObject[], view: TreeView) {
value = value || []
const button = view.registerClick(el => {
view.model.set(path, [...value, this.children.default()])
})
return `<label>${locale(path)}:</label>
<button data-id="${button}">${locale('add')}</button>
<div class="list-fields">
${value.map((obj, index) => {
return this.renderEntry(path.push(index), obj, view)
}).join('')}
</div>`
}
private renderEntry(path: Path, value: IObject, view: TreeView) {
const button = view.registerClick(el => {
view.model.set(path, undefined)
})
return `<div class="list-entry"><button data-id="${button}">${locale('remove')}</button>
${this.children.render(path, value, view, {hideLabel: true})}
</div>`
}
getClassName() {
return 'list-node'
}
}

View File

@@ -1,69 +0,0 @@
import { AbstractNode, NodeMods, INode, StateNode } from './AbstractNode'
import { TreeView } from '../view/TreeView'
import { Path } from '../model/Path'
import { IObject } from './ObjectNode'
import { locale } from '../Registries'
export type IMap = {
[name: string]: IObject
}
/**
* Map nodes similar to list nodes, but a string key is required to add children
*/
export class MapNode extends AbstractNode<IMap> {
protected keys: StateNode<string>
protected values: INode<any>
/**
*
* @param keys node used for the string key
* @param values node used for the map values
* @param mods optional node modifiers
*/
constructor(keys: StateNode<string>, values: INode<any>, mods?: NodeMods<IMap>) {
super({
default: () => ({}),
...mods})
this.keys = keys
this.values = values
}
transform(path: Path, value: IMap) {
if (value === undefined) return undefined
let res: any = {}
Object.keys(value).forEach(f =>
res[f] = this.values.transform(path.push(f), value[f])
)
return this.transformMod(res);
}
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>${locale(path)}:</label>
${this.keys.renderRaw(path, '', view, {hideLabel: true, syncModel: false})}
<button data-id="${button}">${locale('add')}</button>
<div class="map-fields">
${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 class="map-entry"><button data-id="${button}">${locale('remove')}</button>
${this.values.render(path, value, view)}
</div>`
}
getClassName() {
return 'map-node'
}
}

View File

@@ -1,56 +0,0 @@
import { AbstractNode, NodeMods, RenderOptions, StateNode } from './AbstractNode'
import { Path } from '../model/Path'
import { DataModel } from '../model/DataModel'
import { TreeView } from '../view/TreeView'
import { locale } from '../Registries'
export interface NumberNodeMods extends NodeMods<number> {
/** Whether numbers should be converted to integers on input */
integer?: boolean
/** If specified, number will be capped at this minimum */
min?: number
/** If specified, number will be capped at this maximum */
max?: number
}
/**
* Configurable number node with one text field
*/
export class NumberNode extends AbstractNode<number> implements StateNode<number> {
integer: boolean
min: number
max: number
/**
* @param mods optional node modifiers
*/
constructor(mods?: NumberNodeMods) {
super({
default: () => 0,
...mods})
this.integer = mods?.integer ?? false
this.min = mods?.min ?? -Infinity
this.max = mods?.max ?? Infinity
}
getState(el: Element) {
const value = el.querySelector('input')!.value
const parsed = this.integer ? parseInt(value) : parseFloat(value)
if (parsed < this.min) return this.min
if (parsed > this.max) return this.max
return parsed
}
updateModel(el: Element, path: Path, model: DataModel) {
model.set(path, this.getState(el))
}
renderRaw(path: Path, value: number, view: TreeView, options?: RenderOptions) {
return `${options?.hideLabel ? `` : `<label>${locale(path)}</label>`}
<input value="${value ?? ''}">`
}
getClassName() {
return 'number-node'
}
}

View File

@@ -1,100 +0,0 @@
import { NodeMods, INode, NodeChildren, AbstractNode, RenderOptions } from './AbstractNode'
import { Path } from '../model/Path'
import { TreeView } from '../view/TreeView'
import { locale } from '../Registries'
export const Switch = Symbol('switch')
export const Case = Symbol('case')
export type NestedNodeChildren = {
[name: string]: NodeChildren
}
export type IObject = {
[name: string]: any
}
export type FilteredChildren = {
[name: string]: INode<any>
/** The field to filter on */
[Switch]?: string
/** Map of filter values to node fields */
[Case]?: NestedNodeChildren
}
export interface ObjectNodeMods extends NodeMods<object> {
/** Whether the object can be collapsed. Necessary when recursively nesting. */
collapse?: boolean
}
/**
* Object node containing fields with different types.
* Has the ability to filter fields based on a switch field.
*/
export class ObjectNode extends AbstractNode<IObject> {
fields: NodeChildren
cases: NestedNodeChildren
filter?: string
collapse?: boolean
/**
* @param fields children containing the optional switch and case
* @param mods optional node modifiers
*/
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 ?? {}
this.filter = _switch
}
transform(path: Path, value: IObject) {
if (value === undefined) return undefined
const activeCase = this.filter ? this.cases[value[this.filter]] : {};
const activeFields = {...this.fields, ...activeCase}
let res: any = {}
Object.keys(activeFields).forEach(f => {
return res[f] = activeFields[f].transform(path.push(f), value[f])
})
return this.transformMod(res);
}
renderRaw(path: Path, value: IObject, view: TreeView, options?: RenderOptions) {
if (options?.hideLabel) {
return this.renderFields(path, value, view)
} 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}">${locale(path)}</label>`
} else {
const id = view.registerClick(() => view.model.set(path, undefined))
return `<label class="collapse open" data-id="${id}">${locale(path)}</label>
<div class="object-fields">
${this.renderFields(path, value, view)}
</div>`
}
} else {
return `<label>${locale(path)}</label>
<div class="object-fields">
${this.renderFields(path, value, view)}
</div>`
}
}
renderFields(path: Path, value: IObject, view: TreeView) {
value = value ?? {}
const activeCase = this.filter ? this.cases[value[this.filter]] : {};
const activeFields = {...this.fields, ...activeCase}
return Object.keys(activeFields).map(f => {
return activeFields[f].render(path.push(f), value[f], view)
}).join('')
}
getClassName() {
return 'object-node'
}
}

View File

@@ -1,48 +0,0 @@
import { AbstractNode, NodeMods, RenderOptions, INode } from './AbstractNode'
import { TreeView } from '../view/TreeView'
import { Path } from '../model/Path'
import { SCHEMAS } from '../Registries'
export interface AnyNodeMods extends NodeMods<any> {
[name: string]: any
}
/**
* Reference node. Must be used when recursively adding nodes.
*/
export class ReferenceNode extends AbstractNode<any> {
protected reference: () => INode<any>
options: RenderOptions
/**
* @param id schema id that was registered
* @param mods optional node modifiers
*/
constructor(id: string, mods?: AnyNodeMods) {
super(mods)
this.options = {
collapse: mods?.collapse
}
this.reference = () => SCHEMAS.get(id)
}
default(value?: any) {
return this.reference().default(value)
}
transform(path: Path, value: any) {
return this.reference()?.transform(path, value)
}
render(path: Path, value: any, view: TreeView, options?: RenderOptions) {
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, {...this.options, ...options})
}
getClassName() {
return ''
}
}

View File

@@ -1,43 +0,0 @@
import { AbstractNode, NodeMods, RenderOptions, StateNode } from './AbstractNode'
import { Path } from '../model/Path'
import { DataModel } from '../model/DataModel'
import { TreeView } from '../view/TreeView'
import { locale } from '../Registries'
export interface StringNodeMods extends NodeMods<string> {
/** Whether the string can also be empty */
allowEmpty?: boolean
}
/**
* Simple string node with one text field
*/
export class StringNode extends AbstractNode<string> implements StateNode<string> {
allowEmpty: boolean
/**
* @param mods optional node modifiers
*/
constructor(mods?: StringNodeMods) {
super(mods)
this.allowEmpty = mods?.allowEmpty ?? false
}
getState(el: Element) {
return el.querySelector('input')!.value
}
updateModel(el: Element, path: Path, model: DataModel) {
const value = this.getState(el)
model.set(path, this.allowEmpty || value !== '' ? value : undefined)
}
renderRaw(path: Path, value: string, view: TreeView, options?: RenderOptions) {
return `${options?.hideLabel ? `` : `<label>${locale(path)}</label>`}
<input value="${value ?? ''}">`
}
getClassName() {
return 'string-node'
}
}

View File

@@ -1,51 +0,0 @@
import { DataModel, ModelListener } from "../model/DataModel"
import { Path } from "../model/Path"
type SourceViewOptions = {
indentation?: number | string,
rows?: number
}
/**
* JSON representation view of the model.
* Renders the result in a <textarea>.
*/
export class SourceView implements ModelListener {
model: DataModel
target: HTMLElement
options?: SourceViewOptions
/**
* @param model data model this view represents and listens to
* @param target DOM element to render the view
* @param options optional options for the view
*/
constructor(model: DataModel, target: HTMLElement, options?: SourceViewOptions) {
this.model = model
this.target = target
this.options = options
model.addListener(this)
}
render() {
const transformed = this.model.schema.transform(new Path([], this.model), this.model.data)
const textarea = document.createElement('textarea')
textarea.style.width = 'calc(100% - 6px)'
textarea.rows = this.options?.rows ?? 20
textarea.textContent = JSON.stringify(transformed, null, this.options?.indentation)
textarea.addEventListener('change', evt => {
const parsed = JSON.parse(textarea.value)
this.model.reset(parsed)
})
this.target.innerHTML = ''
this.target.appendChild(textarea)
}
/**
* Re-renders the view
* @override
*/
invalidated() {
this.render()
}
}

View File

@@ -1,98 +0,0 @@
import { DataModel, ModelListener } from "../model/DataModel"
import { Path } from "../model/Path"
type Registry = {
[id: string]: (el: Element) => void
}
const registryIdLength = 12
const dec2hex = (dec: number) => ('0' + dec.toString(16)).substr(-2)
/**
* Helper function to generate a random ID
*/
export function getId() {
var arr = new Uint8Array((registryIdLength || 40) / 2)
window.crypto.getRandomValues(arr)
return Array.from(arr, dec2hex).join('')
}
/**
* DOM representation view of the model.
*/
export class TreeView implements ModelListener {
model: DataModel
target: HTMLElement
registry: Registry = {}
/**
* @param model data model this view represents and listens to
* @param target DOM element to render the view
*/
constructor(model: DataModel, target: HTMLElement) {
this.model = model
this.target = target
model.addListener(this)
}
/**
* 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 = getId()
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)
}
render() {
this.target.innerHTML = this.model.schema.render(
new Path(), this.model.data, this, {hideLabel: true})
for (const id in this.registry) {
const element = this.target.querySelector(`[data-id="${id}"]`)
if (element !== null) this.registry[id](element)
}
}
/**
* Re-renders the view
* @override
*/
invalidated(model: DataModel) {
this.render()
}
}

View File

@@ -3,6 +3,7 @@
"target": "es5",
"module": "esnext",
"strict": true,
"moduleResolution": "node",
"esModuleInterop": true,
"downlevelIteration": true,
"forceConsistentCasingInFileNames": true