Updated the native treenode to a custom HTML element

This commit is contained in:
Magnus Åhall 2026-04-29 14:03:08 +02:00
parent d9c82868ab
commit e2b20816c2
3 changed files with 110 additions and 48 deletions

View file

@ -0,0 +1,57 @@
export class CustomHTMLElement extends HTMLElement {
constructor() {// {{{
super()
this.appendChild(this.constructor.tmpl.content.cloneNode(true))
this.querySelectorAll('*').forEach(el => {
const field = el.dataset.field
if (field !== undefined) {
const fieldName = this.toElementName('field', field)
this[fieldName] = el
}
const name = el.dataset.el
if (name !== undefined) {
const elName = this.toElementName('el', name)
this[elName] = el
el.classList.add('el-' + name)
}
})
}// }}}
toElementName(prefix, str) {// {{{
str = prefix + '-' + str
return str.replace(/-(id|[a-z])/g, match => match.toUpperCase().replace('-', ''))
}// }}}
}
export class StupidPreactCustomHTMLElement extends HTMLElement {
constructor() {// {{{
super()
// Stupid stuff because of Preact.
this.clonedNodes = this.constructor.tmpl.content.cloneNode(true)
this.clonedNodes.querySelectorAll('*').forEach(el => {
const field = el.dataset.field
if (field !== undefined) {
const fieldName = this.toElementName('field', field)
this[fieldName] = el
}
const name = el.dataset.el
if (name !== undefined) {
const elName = this.toElementName('el', name)
this[elName] = el
el.classList.add('el-' + name)
}
})
}// }}}
toElementName(prefix, str) {// {{{
str = prefix + '-' + str
return str.replace(/-(id|[a-z])/g, match => match.toUpperCase().replace('-', ''))
}// }}}
connectedCallback() {// {{{
// Stupid stuff because of Preact.
this.appendChild(this.clonedNodes)
}// }}}
}

View file

@ -3,7 +3,7 @@ import { signal } from 'preact/signals'
import htm from 'htm' import htm from 'htm'
import { Node, NodeUI } from 'node' import { Node, NodeUI } from 'node'
import { ROOT_NODE } from 'node_store' import { ROOT_NODE } from 'node_store'
import { TreeNative, TreeNodeNative } from 'tree' import { TreeNative } from 'tree'
const html = htm.bind(h) const html = htm.bind(h)
export class Notes2 extends Component { export class Notes2 extends Component {
@ -542,13 +542,13 @@ class OpSearch extends Op {
for (const r of results) { for (const r of results) {
const ancestors = r.ancestry.reverse().map(a => { const ancestors = r.ancestry.reverse().map(a => {
const div = tmpl(`<div class="ancestor">${a.data.Name}</div>`) const div = tmpl(`<div class="ancestor">${a.data.Name}</div>`)
div[0].addEventListener('click', ()=>_notes2.current.goToNode(a.UUID)) div[0].addEventListener('click', () => _notes2.current.goToNode(a.UUID))
return div[0] return div[0]
}) })
const div = tmpl(`<div>${r.name}</div>`) const div = tmpl(`<div>${r.name}</div>`)
div[0].addEventListener('click', ()=>_notes2.current.goToNode(r.uuid)) div[0].addEventListener('click', () => _notes2.current.goToNode(r.uuid))
rs.push(...div) rs.push(...div)
const ancDev = tmpl('<div class="ancestors"></div>') const ancDev = tmpl('<div class="ancestors"></div>')

View file

@ -1,4 +1,5 @@
import { ROOT_NODE } from 'node_store' import { ROOT_NODE } from 'node_store'
import { CustomHTMLElement } from './lib/custom_html_element.mjs'
export class TreeNative { export class TreeNative {
constructor() {// {{{ constructor() {// {{{
@ -26,14 +27,14 @@ export class TreeNative {
const treeEl = tmpl.content.getElementById('tree-nodes') const treeEl = tmpl.content.getElementById('tree-nodes')
treeEl.addEventListener('keydown', event=>this.keyHandler(event)) treeEl.addEventListener('keydown', event => this.keyHandler(event))
tmpl.content.querySelector('.icons .search').addEventListener('click', ()=>_mbus.dispatch('op-search')) tmpl.content.querySelector('.icons .search').addEventListener('click', () => _mbus.dispatch('op-search'))
tmpl.content.querySelector('.icons .sync').addEventListener('click', ()=>_sync.run()) tmpl.content.querySelector('.icons .sync').addEventListener('click', () => _sync.run())
tmpl.content.getElementById('logo').addEventListener('click', ()=>_app.goToNode(ROOT_NODE, false, false)) tmpl.content.getElementById('logo').addEventListener('click', () => _app.goToNode(ROOT_NODE, false, false))
for (const node of this.treeTrunk) { for (const node of this.treeTrunk) {
const treenode = new TreeNodeNative(this, node) const treenode = new N2TreeNode(this, node)
this.treeNodeComponents[node.UUID] = treenode this.treeNodeComponents[node.UUID] = treenode
treeEl.appendChild(treenode.render()) treeEl.appendChild(treenode.render())
} }
@ -326,47 +327,46 @@ export class TreeNative {
} }
// The ROOT_NODE for example hasn't got a treenode. // The ROOT_NODE for example hasn't got a treenode.
treenode?.element.scrollIntoView({ block: 'nearest' }) treenode?.scrollIntoView({ block: 'nearest' })
}// }}} }// }}}
} }
export class TreeNodeNative { export class N2TreeNode extends CustomHTMLElement {
static {// {{{
this.tmpl = document.createElement('template')
this.tmpl.innerHTML = `
<div data-el="expand-toggle" class="expand-toggle">
<img data-el="expand">
</div>
<div data-el="name" class="name"></div>
<div data-el="children" class="children"></div>
`
}// }}}
constructor(tree, node, parent) {//{{{ constructor(tree, node, parent) {//{{{
super()
this.classList.add('node')
this.tree = tree this.tree = tree
this.node = node this.node = node
this.parent = parent this.parent = parent
this.element = document.createElement('div')
this.element.classList.add('node')
this.icon_expand = document.createElement('img')
this.children_populated = false this.children_populated = false
this.rendered = false this.rendered = false
this.createElements() this.elExpandToggle.addEventListener('click', () => this.tree.setNodeExpanded(this.node, !this.tree.getNodeExpanded(this.node.UUID)))
this.elName.addEventListener('click', () => _mbus.dispatch('TREE_NODE_SELECTED', this.node))
_mbus.subscribe(`NODE_CHILDREN_FETCHED_${node.UUID}`, ()=>{ _mbus.subscribe(`NODE_CHILDREN_FETCHED_${node.UUID}`, () => {
this.render(true) this.render(true)
}) })
_mbus.subscribe(`NODE_EXPAND_${node.UUID}`, state=>{ _mbus.subscribe(`NODE_EXPAND_${node.UUID}`, state => {
this.render(true) this.render(true)
}) })
if (this.node.Level === 0 || this.tree.getNodeExpanded(this.node.UUID)) if (this.node.Level === 0 || this.tree.getNodeExpanded(this.node.UUID))
this.fetchChildren() this.fetchChildren()
}//}}}
createElements() {// {{{
this.element.innerHTML = `
<div class="expand-toggle"></div>
<div class="name" ></div>
<div class="children"></div>
`
this.element.children[0].addEventListener('click', ()=>this.tree.setNodeExpanded(this.node, !this.tree.getNodeExpanded(this.node.UUID)))
this.element.children[0].appendChild(this.icon_expand)
this.element.children[1].addEventListener('click', ()=>_mbus.dispatch('TREE_NODE_SELECTED', this.node))
}// }}} }// }}}
async fetchChildren() {//{{{ async fetchChildren() {//{{{
await this.node.fetchChildren() await this.node.fetchChildren()
@ -374,53 +374,57 @@ export class TreeNodeNative {
}//}}} }//}}}
render(force_update) {//{{{ render(force_update) {//{{{
if (this.rendered && force_update !== true) if (this.rendered && force_update !== true)
return this.element return this
// Fetch the next level of children if the parent tree node is expanded and our children thus will be visible. // Fetch the next level of children if the parent tree node is expanded and our children thus will be visible.
const expanded = this.node.Children.length > 0 && this.tree.getNodeExpanded(this.node.UUID) const expanded = this.node.Children.length > 0 && this.tree.getNodeExpanded(this.node.UUID)
const selected = this.tree.isSelected(this.node) ? 'selected' : ''
if (!this.children_populated && this.tree.getNodeExpanded(this.parent?.node.UUID)) { if (!this.children_populated && this.tree.getNodeExpanded(this.parent?.node.UUID)) {
this.node.fetchChildren().then(()=>this.children_populated = true) this.node.fetchChildren().then(() => this.children_populated = true)
} }
// Update the name and selected status // Update the name and selected status
this.element.children[1].innerText = this.node.get('Name') this.elName.innerText = this.node.get('Name')
this.element.children[1].className = `name ${selected}` if (this.tree.isSelected(this.node))
this.elName.classList.add('selected')
else
this.elName.classList.remove('selected')
// Update expansion state // Update expansion state
this.element.children[2].className = `children ${expanded ? 'expanded' : 'collapsed'}` if (expanded) {
this.elChildren.classList.add('expanded')
// The expand icon <img> is cached to not get a flickering when re-rendering. this.elChildren.classList.remove('collapsed')
if (this.icon_expand === null) } else {
this.icon_expand = document.createElement('img') this.elChildren.classList.remove('expanded')
this.elChildren.classList.add('collapsed')
}
// The expand icon <img> is only changed to not get a flickering when re-rendering.
if (this.node.Children.length === 0) if (this.node.Children.length === 0)
this.setImgSrc(this.icon_expand, `/images/${window._VERSION}/leaf.svg`) this.setImgSrc(this.elExpand, `/images/${window._VERSION}/leaf.svg`)
else if (this.tree.getNodeExpanded(this.node.UUID)) else if (this.tree.getNodeExpanded(this.node.UUID))
this.setImgSrc(this.icon_expand, `/images/${window._VERSION}/expanded.svg`) this.setImgSrc(this.elExpand, `/images/${window._VERSION}/expanded.svg`)
else else
this.setImgSrc(this.icon_expand, `/images/${window._VERSION}/collapsed.svg`) this.setImgSrc(this.elExpand, `/images/${window._VERSION}/collapsed.svg`)
// Should children be rendered? // Should children be rendered?
this.element.children[2].innerHTML = '' this.elChildren.innerHTML = ''
let children = [] let children = []
if (expanded) if (expanded)
children = this.node.Children.map(node => { children = this.node.Children.map(node => {
let treenode = this.tree.treeNodeComponents[node.UUID] let treenode = this.tree.treeNodeComponents[node.UUID]
if (treenode === undefined) { if (treenode === undefined) {
treenode = new TreeNodeNative(this.tree, node, this) treenode = new N2TreeNode(this.tree, node, this)
this.tree.treeNodeComponents[node.UUID] = treenode this.tree.treeNodeComponents[node.UUID] = treenode
} }
return treenode return treenode
//return html`<${TreeNode} key=${`treenode_${node.UUID}`} tree=${tree} node=${node} parent=${this} ref=${tree.treeNodeComponents[node.UUID]} selected=${node.UUID === tree.props.app.startNode?.UUID} />`
}) })
for(const c of children) for (const c of children)
this.element.children[2].appendChild(c.render()) this.elChildren.appendChild(c.render())
this.rendered = true this.rendered = true
return this.element return this
}//}}} }//}}}
setImgSrc(img, newSrc) {// {{{ setImgSrc(img, newSrc) {// {{{
@ -429,5 +433,6 @@ export class TreeNodeNative {
img.setAttribute('src', newSrc) img.setAttribute('src', newSrc)
}// }}} }// }}}
} }
customElements.define('n2-treenode', N2TreeNode)
// vim: foldmethod=marker // vim: foldmethod=marker