import Split from 'split.js'
import {
DataModel,
IView,
TreeView,
SourceView,
ConditionSchema,
LootTableSchema,
AdvancementSchema,
DimensionSchema,
DimensionTypeSchema,
LOCALES,
locale,
COLLECTIONS
} from 'minecraft-schemas'
import { RegistryFetcher } from './RegistryFetcher'
import { SandboxSchema } from './Sandbox'
import { ErrorsView } from './ErrorsView'
const LOCAL_STORAGE_THEME = 'theme'
const modelFromPath = (p: string) => p.split('/').filter(e => e.length !== 0).pop() ?? ''
const addChecked = (el: HTMLElement) => {
el.classList.add('check')
setTimeout(() => {
el.classList.remove('check')
}, 2000)
}
const languages: { [key: string]: string } = {
'en': 'English',
'pt': 'Português',
'ru': 'Русский',
'zh-CN': '简体中文'
}
const registries = [
'attribute',
'biome',
'biome_source',
'block',
'enchantment',
'entity_type',
'fluid',
'item',
'loot_condition_type',
'loot_function_type',
'loot_pool_entry_type',
'mob_effect',
'stat_type',
'structure_feature'
]
const treeViewObserver = (el: HTMLElement) => {
el.querySelectorAll('.node-header[data-error]').forEach(e => {
e.insertAdjacentHTML('beforeend', ``)
})
el.querySelectorAll('.collapse.closed, button.add').forEach(e => {
e.insertAdjacentHTML('afterbegin', ``)
})
el.querySelectorAll('.collapse.open, button.remove').forEach(e => {
e.insertAdjacentHTML('afterbegin', `
`)
})
}
const publicPath = process.env.NODE_ENV === 'production' ? '/dev/' : '/';
Promise.all([
fetch(publicPath + 'locales/schema/en.json').then(r => r.json()),
fetch(publicPath + 'locales/app/en.json').then(r => r.json()),
RegistryFetcher(COLLECTIONS, registries)
]).then(responses => {
LOCALES.register('en', {...responses[0], ...responses[1]})
const selectedModel = document.getElementById('selected-model')!
const modelSelector = document.getElementById('model-selector')!
const modelSelectorMenu = document.getElementById('model-selector-menu')!
const languageSelector = document.getElementById('language-selector')!
const languageSelectorMenu = document.getElementById('language-selector-menu')!
const themeSelector = document.getElementById('theme-selector')!
const treeViewEl = document.getElementById('tree-view')!
const sourceViewEl = document.getElementById('source-view')!
const errorsViewEl = document.getElementById('errors-view')!
const errorsToggle = document.getElementById('errors-toggle')!
const sourceViewOutput = (document.getElementById('source-view-output') as HTMLTextAreaElement)
const treeViewOutput = document.getElementById('tree-view-output')!
const sourceControlsToggle = document.getElementById('source-controls-toggle')!
const sourceControlsMenu = document.getElementById('source-controls-menu')!
const sourceControlsCopy = document.getElementById('source-controls-copy')!
const sourceControlsDownload = document.getElementById('source-controls-download')!
const sourceToggle = document.getElementById('source-toggle')!
const treeControlsToggle = document.getElementById('tree-controls-toggle')!
const treeControlsMenu = document.getElementById('tree-controls-menu')!
const treeControlsVersionToggle = document.getElementById('tree-controls-version-toggle')!
const treeControlsVersionMenu = document.getElementById('tree-controls-version-menu')!
const treeControlsReset = document.getElementById('tree-controls-reset')!
const treeControlsUndo = document.getElementById('tree-controls-undo')!
const treeControlsRedo = document.getElementById('tree-controls-redo')!
let selected = modelFromPath(location.pathname)
const models: { [key: string]: DataModel } = {
'loot-table': new DataModel(LootTableSchema),
'predicate': new DataModel(ConditionSchema),
'advancement': new DataModel(AdvancementSchema),
'dimension': new DataModel(DimensionSchema),
'dimension-type': new DataModel(DimensionTypeSchema),
'sandbox': new DataModel(SandboxSchema)
}
const views: IView[] = [
new TreeView(models[selected], treeViewOutput, {
showErrors: true,
observer: treeViewObserver
}),
new SourceView(models[selected], sourceViewOutput, {
indentation: 2
}),
new ErrorsView(models[selected], errorsViewEl)
]
const updateModel = (newModel: string) => {
selected = newModel
views.forEach(v => v.setModel(models[selected]))
selectedModel.textContent = locale(`title.${selected}`)
modelSelectorMenu.innerHTML = ''
Object.keys(models).forEach(m => {
modelSelectorMenu.insertAdjacentHTML('beforeend',
`
${locale(m)}
`)
modelSelectorMenu.lastChild?.addEventListener('click', evt => {
updateModel(m)
history.pushState({model: m}, m, publicPath + m)
modelSelectorMenu.style.visibility = 'hidden'
})
})
models[selected].invalidate()
}
const updateLanguage = (key: string) => {
LOCALES.language = key
document.querySelectorAll('[data-i18n]').forEach(el => {
el.textContent = locale(el.attributes.getNamedItem('data-i18n')!.value)
})
languageSelectorMenu.innerHTML = ''
Object.keys(languages).forEach(key => {
languageSelectorMenu.insertAdjacentHTML('beforeend',
`${languages[key]}
`)
languageSelectorMenu.lastChild?.addEventListener('click', evt => {
updateLanguage(key)
languageSelectorMenu.style.visibility = 'hidden'
})
})
updateModel(selected)
}
updateLanguage('en')
Split([treeViewEl, sourceViewEl], {
sizes: [66, 34]
})
modelSelector.addEventListener('click', evt => {
modelSelectorMenu.style.visibility = 'visible'
document.body.addEventListener('click', evt => {
modelSelectorMenu.style.visibility = 'hidden'
}, { capture: true, once: true })
})
window.onpopstate = (evt: PopStateEvent) => {
updateModel(modelFromPath(location.pathname))
}
sourceToggle.addEventListener('click', evt => {
if (sourceViewEl.classList.contains('active')) {
sourceViewEl.classList.remove('active')
sourceToggle.classList.remove('toggled')
} else {
sourceViewEl.classList.add('active')
sourceToggle.classList.add('toggled')
}
})
languageSelector.addEventListener('click', evt => {
languageSelectorMenu.style.visibility = 'visible'
document.body.addEventListener('click', evt => {
languageSelectorMenu.style.visibility = 'hidden'
}, { capture: true, once: true })
})
const updateTheme = (theme: string | null) => {
if (theme === null) return
if (theme === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark')
themeSelector.classList.add('toggled')
localStorage.setItem(LOCAL_STORAGE_THEME, 'dark')
} else {
document.documentElement.setAttribute('data-theme', 'light')
themeSelector.classList.remove('toggled')
localStorage.setItem(LOCAL_STORAGE_THEME, 'light')
}
}
updateTheme(localStorage.getItem(LOCAL_STORAGE_THEME))
themeSelector.addEventListener('click', evt => {
if (document.documentElement.getAttribute('data-theme') === 'dark') {
updateTheme('light')
} else {
updateTheme('dark')
}
})
sourceControlsToggle.addEventListener('click', evt => {
sourceControlsMenu.style.visibility = 'visible'
document.body.addEventListener('click', evt => {
sourceControlsMenu.style.visibility = 'hidden'
}, { capture: true, once: true })
})
sourceControlsCopy.addEventListener('click', evt => {
sourceViewOutput.select()
document.execCommand('copy');
addChecked(sourceControlsCopy)
})
sourceControlsDownload.addEventListener('click', evt => {
const fileContents = encodeURIComponent(JSON.stringify(models[selected].data, null, 2) + "\n")
const dataString = "data:text/json;charset=utf-8," + fileContents
const downloadAnchor = document.getElementById('source-controls-download-anchor')!
downloadAnchor.setAttribute("href", dataString)
downloadAnchor.setAttribute("download", "data.json")
downloadAnchor.click()
})
treeControlsToggle.addEventListener('click', evt => {
treeControlsMenu.style.visibility = 'visible'
document.body.addEventListener('click', evt => {
treeControlsMenu.style.visibility = 'hidden'
}, { capture: true, once: true })
})
treeControlsVersionToggle.addEventListener('click', evt => {
treeControlsVersionMenu.style.visibility = 'visible'
document.body.addEventListener('click', evt => {
treeControlsVersionMenu.style.visibility = 'hidden'
}, { capture: true, once: true })
})
treeControlsReset.addEventListener('click', evt => {
models[selected].reset(models[selected].schema.default())
addChecked(treeControlsReset)
})
treeControlsUndo.addEventListener('click', evt => {
models[selected].undo()
})
treeControlsRedo.addEventListener('click', evt => {
models[selected].redo()
})
document.addEventListener('keyup', evt => {
if (evt.ctrlKey && evt.key === 'z') {
models[selected].undo()
} else if (evt.ctrlKey && evt.key === 'y') {
models[selected].redo()
}
})
errorsToggle.addEventListener('click', evt => {
if (errorsViewEl.classList.contains('hidden')) {
errorsViewEl.classList.remove('hidden')
errorsToggle.classList.remove('toggled')
} else {
errorsViewEl.classList.add('hidden')
errorsToggle.classList.add('toggled')
}
})
document.body.style.visibility = 'initial'
})