This commit is contained in:
Magnus Åhall 2025-07-05 07:50:59 +02:00
parent a6bb845c9d
commit cfd5bfd719
9 changed files with 213 additions and 111 deletions

View file

@ -13,12 +13,14 @@ export class App {
this.tree = new Tree(document.getElementById('nodes'))
const events = [
'MENU_ITEM_SELECTED',
'NODE_SELECTED',
'EDITOR_NODE_SAVE',
'TYPES_LIST_FETCHED',
'MENU_ITEM_SELECTED',
'NODE_CREATE_DIALOG',
'NODE_DELETE',
'NODE_EDIT_NAME',
'NODE_SELECTED',
'TREE_RELOAD_NODE',
'TYPES_LIST_FETCHED',
]
for (const eventName of events)
mbus.subscribe(eventName, event => this.eventHandler(event))
@ -45,6 +47,12 @@ export class App {
this.edit(event.detail)
break
case 'NODE_DELETE':
if (!confirm('Are you sure you want to delete this node?'))
return
this.nodeDelete(this.currentNode.ID)
break
case 'EDITOR_NODE_SAVE':
this.nodeUpdate()
break
@ -65,7 +73,18 @@ export class App {
const newName = prompt('Rename node', this.currentNode.Name)
if (newName === null)
return
this.nodeRename(this.currentNode.ID, newName)
.then(() => mbus.dispatch('TREE_RELOAD_NODE', { parentNodeID: this.currentNode.ParentID }))
break
case 'TREE_RELOAD_NODE':
this.tree.updateNode(event.detail.parentNodeID)
.then(() => {
if (event.detail.callback)
event.detail.callback()
.catch(err => showError(err))
})
break
default:
@ -77,6 +96,10 @@ export class App {
let handled = true
switch (event.key.toUpperCase()) {
case 'D':
mbus.dispatch('NODE_DELETE')
break
case 'N':
if (!event.shiftKey || !event.altKey)
return
@ -145,30 +168,38 @@ export class App {
// Name is separate from the JSON node.
const name = document.getElementById('editor-node-name')
name.innerText = json.Node.Name
// The editor-node div is hidden from the start as a lot of the elements
// doesn't make any sense before a node is selected.
document.getElementById('editor-node').style.display = 'grid'
})
}// }}}
nodeRename(nodeID, name) {// {{{
name = name.trim()
if (name.length === 0) {
alert('A name must be provided.')
return
}
async nodeRename(nodeID, name) {// {{{
return new Promise((resolve, reject) => {
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)
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)
resolve()
})
.catch(err => reject(err))
})
}// }}}
nodeUpdate() {// {{{
if (this.editor === null)
@ -198,6 +229,17 @@ export class App {
btn.disabled = false
})
}// }}}
nodeDelete(nodeID) {// {{{
fetch(`/nodes/delete/${nodeID}`)
.then(data => data.json())
.then(json => {
if (!json.OK) {
showError(json.Error)
return
}
})
.catch(err => showError(err))
}// }}}
}
class NodeCreateDialog {
@ -226,10 +268,11 @@ class NodeCreateDialog {
<div style="padding: 16px">
<select></select>
<input type="text" placeholder="Name">
<button onclick="this.commit()">Create</button>
<button>Create</button>
</div>
`
this.dialog.querySelector('button').addEventListener('click', () => this.commit())
this.select = this.dialog.querySelector('select')
this.input = this.dialog.querySelector('input')
this.input.addEventListener('keydown', event => {
@ -261,6 +304,13 @@ class NodeCreateDialog {
showError(json.Error)
return
}
mbus.dispatch('TREE_RELOAD_NODE', {
parentNodeID: this.parentNodeID,
callback: () => {
console.log('hum foo')
mbus.dispatch('NODE_SELECTED', json.NodeID)
},
})
this.dialog.close()
})
.catch(err => showError(err))
@ -359,34 +409,41 @@ export class Tree {
})
}// }}}
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
return new Promise((resolve, reject) => {
// 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
thisTreeNode.node = node
thisTreeNode.updateExpandImages()
thisTreeNode.toggleExpand(true)
// Children are sorted according to type and name.
this.sortChildren(node.Children)
// 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())
// 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
treenode.updateExpandImages()
} else {
const treenode = new TreeNode(n)
this.treeNodes.set(n.ID, treenode)
thisTreeNode.children.appendChild(treenode.render())
}
}
}
resolve()
})
.catch(err => showError(err))
})
.catch(err => reject(err))
})
}// }}}
sortChildren(children) {// {{{
children.sort((a, b) => {
@ -407,6 +464,7 @@ export class TreeNode {
this.childrenFetched = false
this.element = null
this.children = null
this.expandEventListenerAdded = false
}// }}}
render() {// {{{
@ -423,16 +481,13 @@ export class TreeNode {
div.innerHTML = nodeHTML
this.children = div.querySelector('.children')
this.expandImg = div.querySelector('.expand-status img')
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')
this.expandStatus = div.querySelector('.expand-status img')
this.updateExpandImages()
if (this.node.TypeIcon) {
const img = div.querySelector('.type-icon img')
@ -450,15 +505,32 @@ export class TreeNode {
hasChildren() {// {{{
return this.node.NumChildren > 0
}// }}}
toggleExpand(event) {// {{{
const node = event.target.closest('.node')
node?.classList.toggle('expanded')
updateExpandImages() {// {{{
if (this.hasChildren()) {
this.expandStatus.setAttribute('src', `/images/${_VERSION}/node_modules/@mdi/svg/svg/plus-box-outline.svg`)
if (!this.expandEventListenerAdded) {
this.expandStatus.addEventListener('click', () => this.toggleExpand())
this.expandEventListenerAdded = true
}
} else {
this.expandStatus.setAttribute('src', `/images/${_VERSION}/node_modules/@mdi/svg/svg/circle-medium.svg`)
}
}// }}}
toggleExpand(expanded) {// {{{
const node = this.element
if (expanded === undefined)
node?.classList.toggle('expanded')
else if (expanded === true)
node?.classList.add('expanded')
else
node?.classList.remove('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`)
this.expandStatus.setAttribute('src', `/images/${_VERSION}/node_modules/@mdi/svg/svg/${img}.svg`)
if (!this.childrenFetched && this.node.NumChildren > 0 && this.node.Children.length == 0) {
console.log(`fetching for ${this.node.Name}`)
mbus.dispatch('NODE_EXPAND', this)
}
}// }}}