From e2b20816c26d671c0b84a5b5a882d56bc51a7f2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Wed, 29 Apr 2026 14:03:08 +0200 Subject: [PATCH] Updated the native treenode to a custom HTML element --- static/js/lib/custom_html_element.mjs | 57 ++++++++++++++++ static/js/notes2.mjs | 6 +- static/js/tree.mjs | 95 ++++++++++++++------------- 3 files changed, 110 insertions(+), 48 deletions(-) create mode 100644 static/js/lib/custom_html_element.mjs diff --git a/static/js/lib/custom_html_element.mjs b/static/js/lib/custom_html_element.mjs new file mode 100644 index 0000000..dedb5d8 --- /dev/null +++ b/static/js/lib/custom_html_element.mjs @@ -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) + }// }}} +} diff --git a/static/js/notes2.mjs b/static/js/notes2.mjs index 22820f0..7bef2ad 100644 --- a/static/js/notes2.mjs +++ b/static/js/notes2.mjs @@ -3,7 +3,7 @@ import { signal } from 'preact/signals' import htm from 'htm' import { Node, NodeUI } from 'node' import { ROOT_NODE } from 'node_store' -import { TreeNative, TreeNodeNative } from 'tree' +import { TreeNative } from 'tree' const html = htm.bind(h) export class Notes2 extends Component { @@ -542,13 +542,13 @@ class OpSearch extends Op { for (const r of results) { const ancestors = r.ancestry.reverse().map(a => { const div = tmpl(`
${a.data.Name}
`) - div[0].addEventListener('click', ()=>_notes2.current.goToNode(a.UUID)) + div[0].addEventListener('click', () => _notes2.current.goToNode(a.UUID)) return div[0] }) const div = tmpl(`
${r.name}
`) - div[0].addEventListener('click', ()=>_notes2.current.goToNode(r.uuid)) + div[0].addEventListener('click', () => _notes2.current.goToNode(r.uuid)) rs.push(...div) const ancDev = tmpl('
') diff --git a/static/js/tree.mjs b/static/js/tree.mjs index b66736d..43649cc 100644 --- a/static/js/tree.mjs +++ b/static/js/tree.mjs @@ -1,4 +1,5 @@ import { ROOT_NODE } from 'node_store' +import { CustomHTMLElement } from './lib/custom_html_element.mjs' export class TreeNative { constructor() {// {{{ @@ -26,14 +27,14 @@ export class TreeNative { const treeEl = tmpl.content.getElementById('tree-nodes') - treeEl.addEventListener('keydown', event=>this.keyHandler(event)) - tmpl.content.querySelector('.icons .search').addEventListener('click', ()=>_mbus.dispatch('op-search')) - tmpl.content.querySelector('.icons .sync').addEventListener('click', ()=>_sync.run()) + treeEl.addEventListener('keydown', event => this.keyHandler(event)) + tmpl.content.querySelector('.icons .search').addEventListener('click', () => _mbus.dispatch('op-search')) + 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) { - const treenode = new TreeNodeNative(this, node) + const treenode = new N2TreeNode(this, node) this.treeNodeComponents[node.UUID] = treenode treeEl.appendChild(treenode.render()) } @@ -326,47 +327,46 @@ export class TreeNative { } // 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 = ` +
+ +
+
+
+ ` + }// }}} + constructor(tree, node, parent) {//{{{ + super() + this.classList.add('node') + this.tree = tree this.node = node this.parent = parent - this.element = document.createElement('div') - this.element.classList.add('node') - this.icon_expand = document.createElement('img') - this.children_populated = 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) }) - _mbus.subscribe(`NODE_EXPAND_${node.UUID}`, state=>{ + _mbus.subscribe(`NODE_EXPAND_${node.UUID}`, state => { this.render(true) }) if (this.node.Level === 0 || this.tree.getNodeExpanded(this.node.UUID)) this.fetchChildren() - }//}}} - createElements() {// {{{ - this.element.innerHTML = ` -
-
-
- ` - - 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() {//{{{ await this.node.fetchChildren() @@ -374,53 +374,57 @@ export class TreeNodeNative { }//}}} render(force_update) {//{{{ 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. 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)) { - this.node.fetchChildren().then(()=>this.children_populated = true) + this.node.fetchChildren().then(() => this.children_populated = true) } // Update the name and selected status - this.element.children[1].innerText = this.node.get('Name') - this.element.children[1].className = `name ${selected}` + this.elName.innerText = this.node.get('Name') + if (this.tree.isSelected(this.node)) + this.elName.classList.add('selected') + else + this.elName.classList.remove('selected') // Update expansion state - this.element.children[2].className = `children ${expanded ? 'expanded' : 'collapsed'}` - - // The expand icon is cached to not get a flickering when re-rendering. - if (this.icon_expand === null) - this.icon_expand = document.createElement('img') + if (expanded) { + this.elChildren.classList.add('expanded') + this.elChildren.classList.remove('collapsed') + } else { + this.elChildren.classList.remove('expanded') + this.elChildren.classList.add('collapsed') + } + // The expand icon is only changed to not get a flickering when re-rendering. 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)) - this.setImgSrc(this.icon_expand, `/images/${window._VERSION}/expanded.svg`) + this.setImgSrc(this.elExpand, `/images/${window._VERSION}/expanded.svg`) else - this.setImgSrc(this.icon_expand, `/images/${window._VERSION}/collapsed.svg`) + this.setImgSrc(this.elExpand, `/images/${window._VERSION}/collapsed.svg`) // Should children be rendered? - this.element.children[2].innerHTML = '' + this.elChildren.innerHTML = '' let children = [] if (expanded) children = this.node.Children.map(node => { let treenode = this.tree.treeNodeComponents[node.UUID] if (treenode === undefined) { - treenode = new TreeNodeNative(this.tree, node, this) + treenode = new N2TreeNode(this.tree, node, this) this.tree.treeNodeComponents[node.UUID] = 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) - this.element.children[2].appendChild(c.render()) + for (const c of children) + this.elChildren.appendChild(c.render()) this.rendered = true - return this.element + return this }//}}} setImgSrc(img, newSrc) {// {{{ @@ -429,5 +433,6 @@ export class TreeNodeNative { img.setAttribute('src', newSrc) }// }}} } +customElements.define('n2-treenode', N2TreeNode) // vim: foldmethod=marker