import { ROOT_NODE } from 'node_store' import { CustomHTMLElement } from './lib/custom_html_element.mjs' export class N2NodeUI extends CustomHTMLElement { static {// {{{ this.tmpl = document.createElement('template') this.tmpl.innerHTML = `
` }// }}} constructor() {// {{{ super() this.node = null this.style.display = 'contents' _mbus.subscribe('NODE_UI_OPEN', event => { this.node = event.detail.data this.render() }) _mbus.subscribe('NODE_MODIFIED', () => { document.querySelector('#crumbs .crumbs')?.classList.add('node-modified') }) _mbus.subscribe('NODE_UNMODIFIED', () => { document.querySelector('#crumbs .crumbs')?.classList.remove('node-modified') }) this.elNodeContent.addEventListener('input', event => this.contentChanged(event)) }// }}} render() {// {{{ this.elName.innerText = this.node?.get('Name') ?? '' this.elNodeContent.value = this.node?.get('Content') ?? '' }// }}} takeFocus() {// {{{ this.elNodeContent.focus() }// }}} contentChanged(event) {//{{{ this.node.setContent(event.target.value) }//}}} isModified() {// {{{ return this.node?.isModified() }// }}} } customElements.define('n2-nodeui', N2NodeUI) export class Node { static sort(a, b) {//{{{ if (a.data.Name < b.data.Name) return -1 if (a.data.Name > b.data.Name) return 0 return 0 }//}}} static create(name, parentUUID) { return new Node({ UUID: uuidv7(), Created: (new Date()).toISOString(), Content: '', Name: name, ParentUUID: parentUUID, Markdown: false, History: false, }) } constructor(nodeData, level) {//{{{ this.Level = level this.data = nodeData this.UUID = nodeData.UUID // Toplevel nodes are normalized to have the ROOT_NODE as parent. if (nodeData.UUID !== ROOT_NODE && nodeData.ParentUUID === '') { this.ParentUUID = ROOT_NODE this.data.ParentUUID = ROOT_NODE } else this.ParentUUID = nodeData.ParentUUID this._children_fetched = false this.Children = [] this.Ancestors = [] this._sibling_before = null this._sibling_after = null this._parent = null this.reset() /* this.RenderMarkdown = signal(nodeData.RenderMarkdown) this.Markdown = false this.ShowChecklist = signal(false) this._content = nodeData.Content this.Crumbs = [] this.Files = [] this._decrypted = false this._expanded = false // start value for the TreeNode component, this.ChecklistGroups = {} this.ScheduleEvents = signal([]) // it doesn't control it afterwards. // Used to expand the crumbs upon site loading. */ }//}}} reset() {// {{{ this._content = this.data.Content this._modified = false }// }}} get(prop) {//{{{ return this.data[prop] }//}}} updated() {//{{{ // '2024-12-17T17:33:48.85939Z return new Date(Date.parse(this.data.Updated)) }//}}} isModified() {// {{{ return this._modified }// }}} hasFetchedChildren() {//{{{ return this._children_fetched }//}}} async fetchChildren() {//{{{ if (this._children_fetched) return this.Children this.Children = await nodeStore.getTreeNodes(this.UUID, this.Level + 1) this._children_fetched = true // Children are sorted to allow for storing siblings befare and after. // These are used with keyboard navigation in the tree. this.Children.sort(Node.sort) const numChildren = this.Children.length for (let i = 0; i < numChildren; i++) { if (i > 0) this.Children[i]._sibling_before = this.Children[i - 1] if (i < numChildren - 1) this.Children[i]._sibling_after = this.Children[i + 1] this.Children[i]._parent = this } // Notify the tree that all children are fetched and ready to process. //_notes2.current.tree.fetchChildrenOn(this.UUID) _mbus.dispatch(`NODE_CHILDREN_FETCHED_${this.UUID}`) return this.Children }//}}} hasChildren() {//{{{ return this.Children.length > 0 }//}}} getSiblingBefore() {// {{{ return this._sibling_before }// }}} getSiblingAfter() {// {{{ return this._sibling_after }// }}} getParent() {//{{{ return this._parent }//}}} isLastSibling() {//{{{ return this._sibling_after === null }//}}} isFirstSibling() {//{{{ return this._sibling_before === null }//}}} content() {//{{{ /* TODO - implement crypto if (this.CryptoKeyID != 0 && !this._decrypted) this.#decrypt() */ return this._content }//}}} setContent(new_content) {//{{{ this._content = new_content this._modified = true _mbus.dispatch('NODE_MODIFIED') /* TODO - implement crypto if (this.CryptoKeyID == 0) // Logic behind plaintext not being decrypted is that // only encrypted values can be in a decrypted state. this._decrypted = false else this._decrypted = true */ }//}}} async save() {//{{{ this.data.Content = this._content this.data.Updated = new Date().toISOString() this._modified = false _mbus.dispatch('NODE_UNMODIFIED') // When stored into database and ancestry was changed, // the ancestry path could be interesting. const ancestors = await nodeStore.getNodeAncestry(this) this.data.Ancestors = ancestors.map(a => a.get('Name')).reverse() }//}}} } function uuidv7() { // random bytes const value = new Uint8Array(16) crypto.getRandomValues(value) // current timestamp in ms const timestamp = BigInt(Date.now()) // timestamp value[0] = Number((timestamp >> 40n) & 0xffn) value[1] = Number((timestamp >> 32n) & 0xffn) value[2] = Number((timestamp >> 24n) & 0xffn) value[3] = Number((timestamp >> 16n) & 0xffn) value[4] = Number((timestamp >> 8n) & 0xffn) value[5] = Number(timestamp & 0xffn) // version and variant value[6] = (value[6] & 0x0f) | 0x70 value[8] = (value[8] & 0x3f) | 0x80 const str = Array.from(value) .map((b) => b.toString(16).padStart(2, "0")) .join("") return `${str.slice(0, 8)}-${str.slice(8, 12)}-${str.slice(12, 16)}-${str.slice(16, 20)}-${str.slice(20)}` } // vim: foldmethod=marker