import { Editor } from '@editor' import { MessageBus } from '@mbus' export class App { constructor() {// {{{ window.mbus = new MessageBus() this.editor = null this.typesList = null this.currentNodeID = null this.types = [] this.currentPage = null const events = [ 'MENU_ITEM_SELECTED', 'NODE_SELECTED', 'EDITOR_NODE_SAVE', 'TYPES_LIST_FETCHED', 'NODE_CREATE_DIALOG', 'NODE_CREATE', ] for (const eventName of events) mbus.subscribe(eventName, event => this.eventHandler(event)) document.addEventListener('keydown', event => this.keyHandler(event)) mbus.dispatch('MENU_ITEM_SELECTED', 'node') }// }}} async eventHandler(event) {// {{{ switch (event.type) { case 'MENU_ITEM_SELECTED': const item = document.querySelector(`#menu [data-section="${event.detail}"]`) this.page(item, event.detail) break case 'NODE_SELECTED': for (const n of document.querySelectorAll('#nodes .node.selected')) n.classList.remove('selected') for (const n of document.querySelectorAll(`#nodes .node[data-node-id="${event.detail}"]`)) n.classList?.add('selected') this.currentNodeID = event.detail this.edit(this.currentNodeID) break case 'EDITOR_NODE_SAVE': this.nodeUpdate() break case 'TYPES_LIST_FETCHED': const types = document.getElementById('types') types.replaceChildren(this.typesList.render()) case 'NODE_CREATE_DIALOG': if (this.currentPage !== 'node' || this.currentNodeID === null) return new NodeCreateDialog(this.currentNodeID) break case 'NODE_CREATE': break default: console.log(event) } }// }}} keyHandler(event) {// {{{ let handled = true switch (event.key.toUpperCase()) { case 'N': if (!event.shiftKey || !event.altKey) break mbus.dispatch('NODE_CREATE_DIALOG') break default: handled = false } if (handled) { event.stopPropagation() event.preventDefault() } }// }}} page(item, name) {// {{{ for (const el of document.querySelectorAll('#menu .item')) el.classList.remove('selected') item.classList.add('selected') for (const el of document.querySelectorAll('.page.show')) el.classList.remove('show') this.currentPage = name switch (name) { case 'node': document.getElementById('nodes').classList.add('show') document.getElementById('editor-node').classList.add('show') break case 'type': document.getElementById('types').classList.add('show') document.getElementById('editor-type-schema').classList.add('show') if (this.typesList === null) this.typesList = new TypesList() this.typesList.fetchTypes().then(() => { mbus.dispatch('TYPES_LIST_FETCHED') }) break } }// }}} edit(nodeID) {// {{{ console.log(nodeID) fetch(`/nodes/${nodeID}`) .then(data => data.json()) .then(json => { if (!json.OK) { showError(json.Error) return } const editorEl = document.querySelector('#editor-node .editor') this.editor = new Editor(json.Node.TypeSchema) editorEl.replaceChildren(this.editor.render(json.Node.Data)) }) }// }}} nodeUpdate() {// {{{ if (this.editor === null) return const btn = document.querySelector('#editor-node .controls button') btn.disabled = true const buttonPressed = Date.now() const nodeData = this.editor.data() fetch(`/nodes/update/${this.currentNodeID}`, { method: 'POST', body: JSON.stringify(nodeData), }) .then(data => data.json()) .then(json => { if (!json.OK) { showError(json.Error) return } const timePassed = Date.now() - buttonPressed if (timePassed < 250) setTimeout(() => btn.disabled = false, 250 - timePassed) else btn.disabled = false }) }// }}} } class NodeCreateDialog { constructor(parentNodeID) {// {{{ this.parentNodeID = parentNodeID this.dialog = null this.types = null this.select = null this.input = null this.createElements() this.fetchTypes() .then(() => { const st = new SelectType(this.types) this.select.replaceChildren(st.render()) }) this.dialog.showModal() this.select.focus() }// }}} createElements() {// {{{ this.dialog = document.createElement('dialog') this.dialog.id = 'create-type' this.dialog.innerHTML = `
` this.select = this.dialog.querySelector('select') this.input = this.dialog.querySelector('input') this.input.addEventListener('keydown', event => { if (event.key === 'Enter') this.commit() }) document.body.appendChild(this.dialog) }// }}} commit() {// {{{ if (this.input.value.trim().length === 0) { alert('Give a name.') return } const req = { ParentNodeID: this.parentNodeID, TypeID: parseInt(this.select.value), Name: this.input.value.trim(), } fetch('/nodes/create', { method: 'POST', body: JSON.stringify(req), }) .then(data => data.json()) .then(json => { if (!json.OK) { showError(json.Error) return } this.dialog.close() }) .catch(err => showError(err)) }// }}} async fetchTypes() {// {{{ return new Promise((resolve, reject) => { fetch('/types/') .then(data => data.json()) .then(json => { if (!json.OK) { showError(json.Error) return } this.types = json.Types resolve() }) .catch(err => reject(err)) }) }// }}} } class SelectType { constructor(types) {// {{{ this.types = types }// }}} render() {// {{{ const tmpl = document.createElement('template') this.types.sort(typeSort) let prevGroup = null for (const t of this.types) { if (t.Name == 'root_node') continue if (t.Schema['x-group'] != prevGroup) { prevGroup = t.Schema['x-group'] const group = document.createElement('optgroup') group.setAttribute('label', t.Schema['x-group']) tmpl.content.appendChild(group) } const opt = document.createElement('option') opt.setAttribute('value', t.ID) opt.innerText = t.Schema.title || t.Name tmpl.content.appendChild(opt) } return tmpl.content }// }}} } export class TreeNode { constructor(parent, data) {// {{{ this.data = data this.parent = parent this.childrenFetched = false this.children = null this.sortChildren() }// }}} render() {// {{{ const nodeHTML = `
${this.name()}
` const tmpl = document.createElement('template') tmpl.innerHTML = nodeHTML this.children = tmpl.content.querySelector('.children') tmpl.content.querySelector('.name').addEventListener('click', () => mbus.dispatch('NODE_SELECTED', this.data.ID)) // data.NumChildren is set regardless of having fetched the children or not. if (this.hasChildren()) { const img = tmpl.content.querySelector('.expand-status img') img.setAttribute('src', `/images/${_VERSION}/node_modules/@mdi/svg/svg/plus-box-outline.svg`) img.addEventListener('click', event => this.toggleExpand(event)) } else tmpl.content.querySelector('.expand-status').classList.add('leaf') if (this.data.TypeIcon) { const img = tmpl.content.querySelector('.type-icon img') img.setAttribute('src', `/images/${_VERSION}/node_modules/@mdi/svg/svg/${this.data.TypeIcon}.svg`) } this.parent.appendChild(tmpl.content) for (const c of this.data.Children || []) { (new TreeNode(this.children, c)).render() } }// }}} name() {// {{{ if (this.data.TypeName === 'root_node') return 'Start' return this.data.Name }// }}} hasChildren() {// {{{ return this.data.NumChildren > 0 }// }}} sortChildren() {// {{{ this.data.Children.sort((a, b) => { if (a.TypeName < b.TypeName) return -1 if (a.TypeName > b.TypeName) return 1 if (a.Name < b.Name) return -1 if (a.Name > b.Name) return 1 return 0 }) }// }}} toggleExpand(event) {// {{{ const node = event.target.closest('.node') node?.classList.toggle('expanded') const img = node?.classList.contains('expanded') ? 'minus-box-outline' : 'plus-box-outline' event.target.setAttribute('src', `/images/${_VERSION}/node_modules/@mdi/svg/svg/${img}.svg`) if (!this.childrenFetched && this.data.NumChildren > 0 && this.data.Children.length == 0) { this.fetchChildren() .then(data => { this.childrenFetched = true this.data.Children = data.Children this.sortChildren() for (const nodeData of this.data.Children) { const node = new TreeNode(this.children, nodeData) node.render() } }) .catch(err => { alert(err) console.error(err) }) } }// }}} async fetchChildren() {// {{{ return new Promise((resolve, reject) => { fetch(`/nodes/tree/${this.data.ID}?depth=2`) .then(data => data.json()) .then(json => { if (json.OK) resolve(json.Nodes) else reject(json.Error) }) .catch(err => reject(err)) }) }// }}} } export class TypesList { constructur() {// {{{ this.types = [] }// }}} async fetchTypes() {// {{{ return new Promise((resolve, reject) => { fetch('/types/') .then(data => data.json()) .then(json => { if (!json.OK) { showError(json.Error) return } this.types = json.Types resolve() }) .catch(err => reject(err)) }) }// }}} render() {// {{{ const div = document.createElement('div') this.types.sort(typeSort) let prevGroup = null for (const t of this.types) { if (t.Name == 'root_node') continue if (t.Schema['x-group'] != prevGroup) { prevGroup = t.Schema['x-group'] const group = document.createElement('div') group.classList.add('group') group.innerText = t.Schema['x-group'] div.appendChild(group) } const tDiv = document.createElement('div') tDiv.classList.add('type') tDiv.innerHTML = `
${t.Schema.title || t.Name}
` div.appendChild(tDiv) } return div }// }}} } function typeSort(a, b) {// {{{ if (a.Schema['x-group'] === undefined) a.Schema['x-group'] = 'No group' if (b.Schema['x-group'] === undefined) b.Schema['x-group'] = 'No group' if (a.Schema['x-group'] < b.Schema['x-group']) return -1 if (a.Schema['x-group'] > b.Schema['x-group']) return 1 if ((a.Schema.title || a.Name) < (b.Schema.title || b.Name)) return -1 if ((a.Schema.title || a.Name) > (b.Schema.title || b.Name)) return 1 return 0 }// }}} // vim: foldmethod=marker