datagraph/static/js/app.mjs
2025-07-03 13:25:08 +02:00

293 lines
7.2 KiB
JavaScript

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
const events = [
'MENU_ITEM_SELECTED',
'NODE_SELECTED',
'EDITOR_NODE_SAVE',
'TYPES_LIST_FETCHED',
]
for (const eventName of events)
mbus.subscribe(eventName, event => this.eventHandler(event))
mbus.dispatch('MENU_ITEM_SELECTED', 'node')
}// }}}
eventHandler(event) {// {{{
switch (event.type) {
case 'MENU_ITEM_SELECTED':
const item = document.querySelector(`#menu [data-section="${event.detail}"]`)
this.section(item, event.detail)
break
case 'NODE_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())
break
default:
console.log(event)
}
}// }}}
section(item, name) {// {{{
for (const el of document.querySelectorAll('#menu .item'))
el.classList.remove('selected')
item.classList.add('selected')
for (const el of document.querySelectorAll('.section.show'))
el.classList.remove('show')
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
})
}// }}}
}
export class TreeNode {
constructor(parent, data) {// {{{
this.data = data
this.parent = parent
this.childrenFetched = false
this.children = null
this.sortChildren()
}// }}}
render() {// {{{
const nodeHTML = `
<div class="node">
<div class="expand-status"><img /></div>
<div class="type-icon"><img /></div>
<div class="name">${this.name()}</div>
<div class="children"></div>
</div>
`
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((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
})
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 = `
<div class="img"><img src="/images/${_VERSION}/node_modules/@mdi/svg/svg/file-document-check-outline.svg" /></div>
<div class="title">${t.Schema.title || t.Name}</div>
`
div.appendChild(tDiv)
}
return div
}// }}}
}
// vim: foldmethod=marker