Reworked tree
This commit is contained in:
parent
c0255fadb8
commit
a6bb845c9d
9 changed files with 314 additions and 117 deletions
|
|
@ -65,8 +65,20 @@ body {
|
|||
}
|
||||
#editor-node {
|
||||
grid-area: details;
|
||||
display: grid;
|
||||
grid-gap: 16px;
|
||||
grid-template-rows: min-content 1fr min-content;
|
||||
}
|
||||
#editor-node.show {
|
||||
display: grid;
|
||||
}
|
||||
#editor-node > div.ops {
|
||||
display: grid;
|
||||
grid-template-columns: min-content 1fr min-content;
|
||||
align-items: center;
|
||||
grid-gap: 8px;
|
||||
}
|
||||
#editor-node > div.ops #editor-node-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
#types {
|
||||
grid-area: navigation;
|
||||
|
|
|
|||
|
|
@ -6,9 +6,11 @@ export class App {
|
|||
window.mbus = new MessageBus()
|
||||
this.editor = null
|
||||
this.typesList = null
|
||||
this.currentNode = null
|
||||
this.currentNodeID = null
|
||||
this.types = []
|
||||
this.currentPage = null
|
||||
this.tree = new Tree(document.getElementById('nodes'))
|
||||
|
||||
const events = [
|
||||
'MENU_ITEM_SELECTED',
|
||||
|
|
@ -16,7 +18,7 @@ export class App {
|
|||
'EDITOR_NODE_SAVE',
|
||||
'TYPES_LIST_FETCHED',
|
||||
'NODE_CREATE_DIALOG',
|
||||
'NODE_CREATE',
|
||||
'NODE_EDIT_NAME',
|
||||
]
|
||||
for (const eventName of events)
|
||||
mbus.subscribe(eventName, event => this.eventHandler(event))
|
||||
|
|
@ -26,7 +28,7 @@ export class App {
|
|||
mbus.dispatch('MENU_ITEM_SELECTED', 'node')
|
||||
}// }}}
|
||||
|
||||
async eventHandler(event) {// {{{
|
||||
eventHandler(event) {// {{{
|
||||
switch (event.type) {
|
||||
case 'MENU_ITEM_SELECTED':
|
||||
const item = document.querySelector(`#menu [data-section="${event.detail}"]`)
|
||||
|
|
@ -40,8 +42,7 @@ export class App {
|
|||
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)
|
||||
this.edit(event.detail)
|
||||
break
|
||||
|
||||
case 'EDITOR_NODE_SAVE':
|
||||
|
|
@ -51,18 +52,24 @@ export class App {
|
|||
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':
|
||||
case 'NODE_CREATE_DIALOG':
|
||||
if (this.currentPage !== 'node' || this.currentNode === null)
|
||||
return
|
||||
|
||||
new NodeCreateDialog(this.currentNode.ID)
|
||||
break
|
||||
|
||||
case 'NODE_EDIT_NAME':
|
||||
const newName = prompt('Rename node', this.currentNode.Name)
|
||||
if (newName === null)
|
||||
return
|
||||
this.nodeRename(this.currentNode.ID, newName)
|
||||
break
|
||||
|
||||
default:
|
||||
alert(`Unhandled event: ${event.type}`)
|
||||
console.log(event)
|
||||
}
|
||||
}// }}}
|
||||
|
|
@ -72,7 +79,7 @@ export class App {
|
|||
switch (event.key.toUpperCase()) {
|
||||
case 'N':
|
||||
if (!event.shiftKey || !event.altKey)
|
||||
break
|
||||
return
|
||||
mbus.dispatch('NODE_CREATE_DIALOG')
|
||||
break
|
||||
|
||||
|
|
@ -114,8 +121,8 @@ export class App {
|
|||
break
|
||||
}
|
||||
}// }}}
|
||||
|
||||
edit(nodeID) {// {{{
|
||||
console.log(nodeID)
|
||||
fetch(`/nodes/${nodeID}`)
|
||||
.then(data => data.json())
|
||||
.then(json => {
|
||||
|
|
@ -124,10 +131,43 @@ export class App {
|
|||
return
|
||||
}
|
||||
|
||||
this.currentNode = json.Node
|
||||
|
||||
// The JSON editor is created each time. Could probably be reused.
|
||||
const editorEl = document.querySelector('#editor-node .editor')
|
||||
this.editor = new Editor(json.Node.TypeSchema)
|
||||
editorEl.replaceChildren(this.editor.render(json.Node.Data))
|
||||
|
||||
if (json.Node.Data['x-new'])
|
||||
editorEl.replaceChildren(this.editor.render(null))
|
||||
else
|
||||
editorEl.replaceChildren(this.editor.render(json.Node.Data))
|
||||
|
||||
// Name is separate from the JSON node.
|
||||
const name = document.getElementById('editor-node-name')
|
||||
name.innerText = json.Node.Name
|
||||
})
|
||||
}// }}}
|
||||
nodeRename(nodeID, name) {// {{{
|
||||
name = name.trim()
|
||||
if (name.length === 0) {
|
||||
alert('A name must be provided.')
|
||||
return
|
||||
}
|
||||
|
||||
fetch(`/nodes/rename/${nodeID}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
Name: name,
|
||||
}),
|
||||
})
|
||||
.then(data => data.json())
|
||||
.then(json => {
|
||||
if (!json.OK) {
|
||||
showError(json.Error)
|
||||
return
|
||||
}
|
||||
|
||||
this.edit(nodeID)
|
||||
})
|
||||
}// }}}
|
||||
nodeUpdate() {// {{{
|
||||
|
|
@ -140,7 +180,7 @@ export class App {
|
|||
|
||||
const nodeData = this.editor.data()
|
||||
|
||||
fetch(`/nodes/update/${this.currentNodeID}`, {
|
||||
fetch(`/nodes/update/${this.currentNode.ID}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(nodeData),
|
||||
})
|
||||
|
|
@ -186,7 +226,7 @@ class NodeCreateDialog {
|
|||
<div style="padding: 16px">
|
||||
<select></select>
|
||||
<input type="text" placeholder="Name">
|
||||
<button onclick="mbus.dispatch('NODE_CREATE', ()=>this.commit())">Create</button>
|
||||
<button onclick="this.commit()">Create</button>
|
||||
</div>
|
||||
`
|
||||
|
||||
|
|
@ -273,61 +313,83 @@ class SelectType {
|
|||
}// }}}
|
||||
}
|
||||
|
||||
export class TreeNode {
|
||||
constructor(parent, data) {// {{{
|
||||
this.data = data
|
||||
this.parent = parent
|
||||
this.childrenFetched = false
|
||||
this.children = null
|
||||
export class Tree {
|
||||
constructor() {// {{{
|
||||
this.treeNodes = new Map()
|
||||
|
||||
this.sortChildren()
|
||||
const events = [
|
||||
'NODE_EXPAND',
|
||||
]
|
||||
for (const e of events)
|
||||
mbus.subscribe(e, event => this.eventHandler(event))
|
||||
|
||||
|
||||
this.fetchNodes(0)
|
||||
.then(node => {
|
||||
const top = document.getElementById('nodes')
|
||||
const topNode = new TreeNode(node)
|
||||
this.treeNodes.set(node.ID, topNode)
|
||||
top.appendChild(topNode.render())
|
||||
this.updateNode(0)
|
||||
})
|
||||
.catch(err => showError(err))
|
||||
}// }}}
|
||||
eventHandler(event) {// {{{
|
||||
switch (event.type) {
|
||||
case 'NODE_EXPAND':
|
||||
this.updateNode(event.detail.node.ID)
|
||||
break
|
||||
|
||||
render() {// {{{
|
||||
const nodeHTML = `
|
||||
<div class="node" data-node-id="${this.data.ID}">
|
||||
<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()
|
||||
default:
|
||||
alert(`Unhandled event: ${event.type}`)
|
||||
console.log(event)
|
||||
}
|
||||
}// }}}
|
||||
name() {// {{{
|
||||
if (this.data.TypeName === 'root_node')
|
||||
return 'Start'
|
||||
return this.data.Name
|
||||
async fetchNodes(topNode) {// {{{
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch(`/nodes/tree/${topNode}?depth=1`)
|
||||
.then(data => data.json())
|
||||
.then(json => {
|
||||
if (!json.OK) {
|
||||
reject(json.Error)
|
||||
return
|
||||
}
|
||||
resolve(json.Nodes)
|
||||
})
|
||||
})
|
||||
}// }}}
|
||||
hasChildren() {// {{{
|
||||
return this.data.NumChildren > 0
|
||||
updateNode(nodeID) {// {{{
|
||||
// updateNode retrieves a node and its' immediate children.
|
||||
// Node and each child is found in the treeNodes map and the names are updated.
|
||||
// If not found, created and added.
|
||||
//
|
||||
// Newly created nodes are found and added, existing but renamed nodes are modified, and unchanged are left as is.
|
||||
this.fetchNodes(nodeID)
|
||||
.then(node => {
|
||||
const thisTreeNode = this.treeNodes.get(nodeID)
|
||||
thisTreeNode.childrenFetched = true
|
||||
|
||||
// Children are sorted according to type and name.
|
||||
this.sortChildren(node.Children)
|
||||
|
||||
// Update or add children
|
||||
for (const n of node.Children) {
|
||||
if (this.treeNodes.has(n.ID)) {
|
||||
const treenode = this.treeNodes.get(n.ID)
|
||||
treenode.node = n
|
||||
treenode.element.querySelector('.name').innerText = n.Name
|
||||
} else {
|
||||
const treenode = new TreeNode(n)
|
||||
this.treeNodes.set(n.ID, treenode)
|
||||
thisTreeNode.children.appendChild(treenode.render())
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
.catch(err => showError(err))
|
||||
}// }}}
|
||||
sortChildren() {// {{{
|
||||
this.data.Children.sort((a, b) => {
|
||||
sortChildren(children) {// {{{
|
||||
children.sort((a, b) => {
|
||||
if (a.TypeName < b.TypeName) return -1
|
||||
if (a.TypeName > b.TypeName) return 1
|
||||
|
||||
|
|
@ -337,7 +399,57 @@ export class TreeNode {
|
|||
return 0
|
||||
})
|
||||
}// }}}
|
||||
}
|
||||
|
||||
export class TreeNode {
|
||||
constructor(data) {// {{{
|
||||
this.node = data
|
||||
this.childrenFetched = false
|
||||
this.element = null
|
||||
this.children = null
|
||||
}// }}}
|
||||
|
||||
render() {// {{{
|
||||
const nodeHTML = `
|
||||
<div class="expand-status"><img /></div>
|
||||
<div class="type-icon"><img /></div>
|
||||
<div class="name">${this.name()}</div>
|
||||
<div class="children"></div>
|
||||
`
|
||||
|
||||
const div = document.createElement('div')
|
||||
div.classList.add('node')
|
||||
div.setAttribute('data-node-id', this.node.ID)
|
||||
div.innerHTML = nodeHTML
|
||||
|
||||
this.children = div.querySelector('.children')
|
||||
|
||||
div.querySelector('.name').addEventListener('click', () => mbus.dispatch('NODE_SELECTED', this.node.ID))
|
||||
|
||||
// data.NumChildren is set regardless of having fetched the children or not.
|
||||
if (this.hasChildren()) {
|
||||
const img = div.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
|
||||
div.querySelector('.expand-status').classList.add('leaf')
|
||||
|
||||
if (this.node.TypeIcon) {
|
||||
const img = div.querySelector('.type-icon img')
|
||||
img.setAttribute('src', `/images/${_VERSION}/node_modules/@mdi/svg/svg/${this.node.TypeIcon}.svg`)
|
||||
}
|
||||
|
||||
this.element = div
|
||||
return div
|
||||
}// }}}
|
||||
name() {// {{{
|
||||
if (this.node.TypeName === 'root_node')
|
||||
return 'Start'
|
||||
return this.node.Name
|
||||
}// }}}
|
||||
hasChildren() {// {{{
|
||||
return this.node.NumChildren > 0
|
||||
}// }}}
|
||||
toggleExpand(event) {// {{{
|
||||
const node = event.target.closest('.node')
|
||||
node?.classList.toggle('expanded')
|
||||
|
|
@ -345,27 +457,14 @@ export class TreeNode {
|
|||
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)
|
||||
})
|
||||
if (!this.childrenFetched && this.node.NumChildren > 0 && this.node.Children.length == 0) {
|
||||
console.log(`fetching for ${this.node.Name}`)
|
||||
mbus.dispatch('NODE_EXPAND', this)
|
||||
}
|
||||
}// }}}
|
||||
async fetchChildren() {// {{{
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch(`/nodes/tree/${this.data.ID}?depth=2`)
|
||||
fetch(`/nodes/tree/${this.node.ID}?depth=1`)
|
||||
.then(data => data.json())
|
||||
.then(json => {
|
||||
if (json.OK)
|
||||
|
|
|
|||
|
|
@ -5,22 +5,39 @@ export class Editor {
|
|||
}
|
||||
|
||||
render(data) {
|
||||
const div = document.createElement('div')
|
||||
this.editor = new JSONEditor(div, {
|
||||
const options = {
|
||||
theme: 'spectre',
|
||||
iconlib: 'spectre',
|
||||
disable_collapse: true,
|
||||
disable_properties: true,
|
||||
schema: this.schema,
|
||||
});
|
||||
}
|
||||
|
||||
this.editor.on('ready', () => {
|
||||
this.editor.setValue(data)
|
||||
})
|
||||
// startval isn't set if this is a newly created node.
|
||||
// When setValue is called (or startval set), all widgets/fields are hidden if not defined in the JSON data.
|
||||
// When startval isn't set, the schema properties are displayed instead.
|
||||
if (data !== undefined && data !== null)
|
||||
options.startval = data
|
||||
|
||||
const div = document.createElement('div')
|
||||
this.editor = new JSONEditor(div, options);
|
||||
|
||||
|
||||
// this.editor.on('ready', ()=>{
|
||||
// })
|
||||
div.addEventListener('keydown', event=>this.keyHandler(event))
|
||||
|
||||
return div
|
||||
}
|
||||
|
||||
keyHandler(event) {
|
||||
if (!event.ctrlKey || event.key != 's')
|
||||
return
|
||||
mbus.dispatch('EDITOR_NODE_SAVE')
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
data() {
|
||||
return this.editor.getValue()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,8 +86,27 @@ body {
|
|||
|
||||
#editor-node {
|
||||
grid-area: details;
|
||||
display: grid;
|
||||
grid-gap: 16px;
|
||||
grid-template-rows:
|
||||
min-content
|
||||
1fr
|
||||
min-content
|
||||
;
|
||||
|
||||
&.show {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
& > div.ops {
|
||||
display: grid;
|
||||
grid-template-columns: min-content 1fr min-content;
|
||||
align-items: center;
|
||||
grid-gap: 8px;
|
||||
|
||||
#editor-node-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#types {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue