mirror of
https://github.com/misode/misode.github.io.git
synced 2026-04-23 23:27:09 +00:00
Move schemas to npm package
This commit is contained in:
@@ -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()
|
||||
}
|
||||
@@ -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']
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
])
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
)
|
||||
}))
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
@@ -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 ''
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user