Reworked tree rendering and moving

This commit is contained in:
Magnus Åhall 2025-07-07 20:01:30 +02:00
parent c7b0823900
commit d450418bf6
3 changed files with 91 additions and 100 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
datagraph

BIN
datagraph

Binary file not shown.

View file

@ -257,6 +257,9 @@ export class App {
})
}// }}}
nodeDelete(nodeID) {// {{{
const node = this.tree.treeNodes.get(nodeID)
const parentID = node.node.ParentID
fetch(`/nodes/delete/${nodeID}`)
.then(data => data.json())
.then(json => {
@ -264,6 +267,8 @@ export class App {
showError(json.Error)
return
}
this.tree.updateNode(parseInt(parentID))
})
.catch(err => showError(err))
}// }}}
@ -283,36 +288,13 @@ export class App {
return
}
const parentTreenode = this.tree.treeNodes.get(newParentID)
const node = await this.tree.fetchNodes(newParentID)
parentTreenode.node = node
parentTreenode.updateExpandImages()
const newParentElement = this.tree.treeNodes.get(newParentID).children
for (const n of nodes) {
const movedTreeNode = this.tree.treeNodes.get(n.ID)
newParentElement.append(movedTreeNode.element)
// Moved nodes' parents are updated to remove the moved nodes from their children.
const treenode = this.tree.treeNodes.get(n.ParentID)
const node = await this.tree.fetchNodes(n.ParentID)
treenode.node = node
treenode.updateExpandImages()
// Children are resorted to get the moved node into correct order.
/*
this.tree.sortChildren(parentTreenode.node.Children)
for (const c of parentTreenode.node.Children) {
const treenode = this.tree.treeNodes.get(c.ID)
if (treenode)
parentTreenode.children.append(treenode.element)
}
*/
// The moved node is updated with its new parent ID for future moves.
movedTreeNode.node.ParentID = newParentID
}
const updateParents = new Map()
for (const n of nodes)
updateParents.set(n.ParentID, true)
updateParents.set(newParentID, true)
for (const nodeID of updateParents.keys())
this.tree.updateNode(nodeID)
})
.catch(err => showError(err))
}// }}}
@ -465,10 +447,10 @@ export class Tree {
this.fetchNodes(0)
.then(node => {
const top = document.getElementById('nodes')
const topNode = new TreeNode(node)
this.treeNodes.set(node.ID, topNode)
const topNode = this.treeNodes.get(0)
topNode.expanded = true
top.appendChild(topNode.render())
this.updateNode(0)
//this.updateNode(0)
})
.catch(err => showError(err))
}// }}}
@ -492,6 +474,28 @@ export class Tree {
reject(json.Error)
return
}
// Make sure treenodes are updated with the latest fetched data.
// The parent node is processed after the children, since the render function needs to have the children already available.
const nodes = []
nodes.push(...json.Nodes.Children)
nodes.push(json.Nodes)
for (const n of nodes) {
let treenode = this.treeNodes.get(n.ID)
if (treenode === undefined) {
treenode = new TreeNode(this, n)
treenode.render()
this.treeNodes.set(n.ID, treenode)
} else {
// Since the depth is set to 1, the childrens' children array will be empty.
// If children have been fetched, these should be kept.
if (n.NumChildren > 0 && n.Children.length == 0)
n.Children = treenode.node.Children
treenode.node = n
}
}
resolve(json.Nodes)
})
})
@ -503,41 +507,13 @@ export class Tree {
// 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)
this.fetchNodes(nodeID, true)
.then(node => {
const thisTreeNode = this.treeNodes.get(nodeID)
thisTreeNode.childrenFetched = true
thisTreeNode.node = node
thisTreeNode.updateExpandImages()
thisTreeNode.toggleExpand(true)
thisTreeNode.render()
// Children are sorted according to type and name.
this.sortChildren(node.Children)
// Deleted or moved children
for (const c of thisTreeNode.children.children) {
const nodeID = parseInt(c.dataset.nodeId)
const nodeStillExist = node.Children.some(n => n.ID === nodeID)
if (!nodeStillExist) {
c.remove()
mbus.dispatch('NODE_REMOVED', nodeID)
}
}
// 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())
}
}
for (const n of thisTreeNode.node.Children)
this.treeNodes.get(n.ID)?.render()
resolve()
@ -568,82 +544,96 @@ export class Tree {
}
export class TreeNode {
constructor(data) {// {{{
constructor(tree, data) {// {{{
this.tree = tree
this.node = data
this.childrenFetched = false
this.element = null
this.children = null
this.nameElement = null
this.expandEventListenerAdded = false
this.expanded = false
}// }}}
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>
`
if (this.element === null) {
this.element = document.createElement('div')
this.element.classList.add('node')
this.element.setAttribute('data-node-id', this.node.ID)
const div = document.createElement('div')
div.classList.add('node')
div.setAttribute('data-node-id', this.node.ID)
div.innerHTML = nodeHTML
const nodeHTML = `
<div class="expand-status"><img /></div>
<div class="type-icon"><img /></div>
<div class="name"></div>
<div class="children"></div>
`
this.element.innerHTML = nodeHTML
this.children = div.querySelector('.children')
this.expandImg = div.querySelector('.expand-status img')
this.nameElement = this.element.querySelector('.name')
this.children = this.element.querySelector('.children')
this.expandImg = this.element.querySelector('.expand-status img')
this.expandStatus = this.element.querySelector('.expand-status img')
div.querySelector('.name').addEventListener('click', event => {
if (!event.shiftKey)
mbus.dispatch('NODE_SELECTED', this.node.ID)
else
this.element.classList.toggle('marked')
event.stopPropagation()
})
this.nameElement.addEventListener('click', event => this.clickNode(event))
}
// data.NumChildren is set regardless of having fetched the children or not.
this.expandStatus = div.querySelector('.expand-status img')
this.updateExpandImages()
if (this.node.TypeIcon) {
const img = div.querySelector('.type-icon img')
const img = this.element.querySelector('.type-icon img')
img.setAttribute('src', `/images/${_VERSION}/node_modules/@mdi/svg/svg/${this.node.TypeIcon}.svg`)
}
this.element = div
return div
this.nameElement.innerText = this.name()
this.tree.sortChildren(this.node.Children)
const children = []
for (const c of this.node.Children)
children.push(this.tree.treeNodes.get(c.ID).element)
this.children.replaceChildren(...children)
return this.element
}// }}}
name() {// {{{
if (this.node.TypeName === 'root_node')
return 'Start'
return this.node.Name
}// }}}
clickNode(event) {// {{{
if (!event.shiftKey)
mbus.dispatch('NODE_SELECTED', this.node.ID)
else
this.element.classList.toggle('marked')
event.stopPropagation()
}// }}}
hasChildren() {// {{{
return this.node.NumChildren > 0
}// }}}
updateExpandImages() {// {{{
if (this.hasChildren()) {
this.expandStatus.setAttribute('src', `/images/${_VERSION}/node_modules/@mdi/svg/svg/plus-box-outline.svg`)
if (this.expanded)
this.element.classList.add('expanded')
else
this.element.classList.remove('expanded')
this.expandStatus.setAttribute('src', `/images/${_VERSION}/node_modules/@mdi/svg/svg/${this.expanded ? 'minus-box-outline.svg' : 'plus-box-outline.svg'}`)
if (!this.expandEventListenerAdded) {
this.expandStatus.addEventListener('click', () => this.toggleExpand())
this.expandEventListenerAdded = true
}
} else {
} 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')
this.expanded = !this.expanded
else
node?.classList.remove('expanded')
this.expanded = expanded
const img = node?.classList.contains('expanded') ? 'minus-box-outline' : 'plus-box-outline'
this.expandStatus.setAttribute('src', `/images/${_VERSION}/node_modules/@mdi/svg/svg/${img}.svg`)
this.updateExpandImages()
if (!this.childrenFetched && this.node.NumChildren > 0 && this.node.Children.length == 0) {
mbus.dispatch('NODE_EXPAND', this)