import Split from 'split.js' import { Base, DataModel, locale, LOCALES, ModelPath, SourceView, TreeView, Path, } from '@mcschema/core' import { getCollections, getSchemas } from '@mcschema/java-1.16' import { VisualizerView } from './visualization/VisualizerView' import { RegistryFetcher } from './RegistryFetcher' import { ErrorsView } from './ErrorsView' import config from '../config.json' import { BiomeNoiseVisualizer } from './visualization/BiomeNoiseVisualizer' const LOCAL_STORAGE_THEME = 'theme' const LOCAL_STORAGE_LANGUAGE = 'language' const publicPath = process.env.NODE_ENV === 'production' ? '/dev/' : '/'; const modelFromPath = (p: string) => p.replace(publicPath, '').replace(/\/$/, '') const addChecked = (el: HTMLElement) => { el.classList.add('check') setTimeout(() => { el.classList.remove('check') }, 2000) } const treeViewObserver = (el: HTMLElement) => { el.querySelectorAll('.node-header[data-help]').forEach(e => { const div = document.createElement('div') div.className = 'node-icon' div.addEventListener('click', evt => { div.getElementsByTagName('span')[0].classList.add('show') document.body.addEventListener('click', evt => { div.getElementsByTagName('span')[0].classList.remove('show') }, { capture: true, once: true }) }) div.insertAdjacentHTML('beforeend', `${e.getAttribute('data-help')}`) e.appendChild(div) }) el.querySelectorAll('.node-header[data-error]').forEach(e => { const div = document.createElement('div') div.className = 'node-icon' div.addEventListener('click', evt => { div.getElementsByTagName('span')[0].classList.add('show') document.body.addEventListener('click', evt => { div.getElementsByTagName('span')[0].classList.remove('show') }, { capture: true, once: true }) }) div.insertAdjacentHTML('beforeend', `${e.getAttribute('data-error')}`) e.appendChild(div) }) el.querySelectorAll('.collapse.closed, button.add').forEach(e => { e.insertAdjacentHTML('afterbegin', ``) }) el.querySelectorAll('.collapse.open, button.remove').forEach(e => { e.insertAdjacentHTML('afterbegin', ``) }) } const treeViewNodeInjector = (path: ModelPath, view: TreeView) => { let res = VisualizerView.visualizers .filter(v => v.active(path)) .map(v => { const id = view.registerClick(() => { views.visualizer.set(v, path) }) return `` }) .join('') if (views.visualizer.active && views.visualizer.visualizer?.getName() === 'biome-noise') { if (path.startsWith(new Path(['generator', 'biome_source', 'biomes'])) && path.getArray().length === 4) { const biomeVisualizer = views.visualizer.visualizer as BiomeNoiseVisualizer const biome = path.push('biome').get() const id = view.registerChange(el => { biomeVisualizer.setBiomeColor(biome, (el as HTMLInputElement).value) views.visualizer.visualizer!.state = {} views.visualizer.invalidated() }) res += `` } } return res } const fetchLocale = async (id: string) => { const response = await fetch(publicPath + `locales/${id}.json`) LOCALES.register(id, await response.json()) } LOCALES.language = localStorage.getItem(LOCAL_STORAGE_LANGUAGE)?.toLowerCase() ?? 'en' const homeLink = document.getElementById('home-link')! const homeGenerators = document.getElementById('home-generators')! const categoryGenerators = document.getElementById('category-generators')! 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 homeViewEl = document.getElementById('home-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 treeControlsReset = document.getElementById('tree-controls-reset')! const treeControlsUndo = document.getElementById('tree-controls-undo')! const treeControlsRedo = document.getElementById('tree-controls-redo')! const visualizerOutput = document.getElementById('visualizer-output')! Split([treeViewEl, sourceViewEl], { sizes: [66, 34] }) Split([sourceViewOutput, visualizerOutput], { sizes: [60, 40], direction: 'vertical' }) const dummyModel = new DataModel(Base) const views = { 'tree': new TreeView(dummyModel, treeViewOutput, { showErrors: true, observer: treeViewObserver, nodeInjector: treeViewNodeInjector }), 'source': new SourceView(dummyModel, sourceViewOutput, { indentation: 2 }), 'errors': new ErrorsView(dummyModel, errorsViewEl), 'visualizer': new VisualizerView(dummyModel, visualizerOutput as HTMLCanvasElement) } const COLLECTIONS = getCollections() Promise.all([ fetchLocale(LOCALES.language), ...(LOCALES.language === 'en' ? [] : [fetchLocale('en')]), RegistryFetcher(COLLECTIONS, config.registries) ]).then(responses => { const SCHEMAS = getSchemas(COLLECTIONS) let models: { [key: string]: DataModel } = {} const buildModel = (model: any) => { if (model.schema) { models[model.id] = new DataModel(SCHEMAS.get(model.schema)) } else if (model.children) { model.children.forEach(buildModel) } } config.models.forEach(buildModel) let selected = '' Object.values(models).forEach(m => m.validate(true)) const updateModel = () => { let title = '' if (models[selected] === undefined) { title = locale('title.home') } else { title = locale('title.generator', [locale(selected)]) Object.values(views).forEach(v => v.setModel(models[selected])) models[selected].invalidate() } selectedModel.textContent = title document.title = title modelSelectorMenu.innerHTML = '' Object.keys(models).forEach(m => { modelSelectorMenu.insertAdjacentHTML('beforeend', `
${locale(m)}
`) modelSelectorMenu.lastChild?.addEventListener('click', evt => { reload(publicPath + m) }) }) } const updateLanguage = (id: string, store = false) => { LOCALES.language = id if (store) { localStorage.setItem(LOCAL_STORAGE_LANGUAGE, id) } document.querySelectorAll('[data-i18n]').forEach(el => { el.textContent = locale(el.attributes.getNamedItem('data-i18n')!.value) }) languageSelectorMenu.innerHTML = '' config.languages.forEach(lang => { languageSelectorMenu.insertAdjacentHTML('beforeend', `
${lang.name}
`) languageSelectorMenu.lastChild?.addEventListener('click', evt => { updateLanguage(lang.code, true) languageSelectorMenu.style.visibility = 'hidden' }) }) if (LOCALES.has(id)) { updateModel() } else { fetchLocale(id).then(r => { updateModel() }) } } homeLink.addEventListener('click', evt => { reload(publicPath) }) 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) => { reload(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) => { ga('set', 'dimension1', theme ?? 'default'); 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 }) }) treeControlsReset.addEventListener('click', evt => { models[selected].reset(models[selected].schema.default(), true) 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('active')) { errorsViewEl.classList.remove('active') errorsToggle.classList.remove('toggled') } else { errorsViewEl.classList.add('active') errorsToggle.classList.add('toggled') } }) const reload = (target: string, track=true) => { if (!target.endsWith('/')) { target = `${target}/` } if (track) { ga('set', 'page', target.replace(/^\/dev/, '')) ga('send', 'pageview'); history.pushState(target, 'Change Page', target) } selected = modelFromPath(target) ?? '' const panels = [treeViewEl, sourceViewEl, errorsViewEl] if (models[selected] === undefined) { homeViewEl.style.display = ''; (document.querySelector('.gutter') as HTMLElement).style.display = 'none'; (document.querySelector('.content') as HTMLElement).style.overflowY = 'initial' modelSelector.style.display = 'none' panels.forEach(v => v.style.display = 'none') const addGen = (output: HTMLElement) => (m: any) => { output.insertAdjacentHTML('beforeend', `
${locale(m.name)} ${m.schema ? '' : ''}
`) output.lastChild?.addEventListener('click', evt => { reload(publicPath + m.id) }) } homeGenerators.innerHTML = '' categoryGenerators.innerHTML = '' config.models.forEach(addGen(homeGenerators)) config.models.find(m => m.id === selected)?.children?.forEach(addGen(categoryGenerators)) } else { homeViewEl.style.display = 'none'; (document.querySelector('.gutter') as HTMLElement).style.display = '' modelSelector.style.display = '' panels.forEach(v => v.style.display = '') } updateLanguage(LOCALES.language) } reload(location.pathname, false) document.body.style.visibility = 'initial' })