import { ROOT_NODE } from 'node_store' import { CustomHTMLElement } from './lib/custom_html_element.mjs' import { MarkedPosition } from './marked_position.mjs' export class N2NodeUI extends CustomHTMLElement { static {// {{{ this.tmpl = document.createElement('template') this.tmpl.innerHTML = `
` }// }}} constructor() {// {{{ super() this.node = null this.style.display = 'contents' this.classList.add('show-markdown') // TODO Should probably be moved to settings. this.marked = new MarkedPosition() _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') this.elIconSave.src = `/images/${_VERSION}/icon_save.svg` }) _mbus.subscribe('NODE_UNMODIFIED', () => { document.querySelector('#crumbs .crumbs')?.classList.remove('node-modified') this.elIconSave.src = `/images/${_VERSION}/icon_save_disabled.svg` }) _mbus.subscribe('MARKDOWN_TOGGLE', () => this.showMarkdown(!this.showMarkdown())) _mbus.subscribe('MARKDOWN_EDIT', ({ detail }) => this.editMarkdown(detail.data)) this.elNodeContent.addEventListener('input', event => this.contentChanged(event)) this.elIconMarkdown.addEventListener('click', () => this.showMarkdown(!this.showMarkdown())) this.showMarkdown(true) }// }}} render() {// {{{ this.elName.innerText = this.node?.get('Name') ?? '' this.elNodeContent.value = this.node?.get('Content') ?? '' this.elNodeMarkdown.innerHTML = this.marked.parse(this.elNodeContent.value) }// }}} takeFocus() {// {{{ console.log('taking focus', this.showMarkdown()) if (this.showMarkdown()) { this.elNodeMarkdown.focus() console.log(this.elNodeMarkdown) } else this.elNodeContent.focus() }// }}} contentChanged(event) {//{{{ this.node.setContent(event.target.value) }//}}} isModified() {// {{{ return this.node?.isModified() }// }}} showMarkdown(state) {// {{{ switch (state) { case true: this.elNodeMarkdown.innerHTML = this.marked.parse(this.elNodeContent.value) this.elIconMarkdown.src = `/images/${_VERSION}/icon_markdown.svg` this.classList.add('show-markdown') break case false: this.elIconMarkdown.src = `/images/${_VERSION}/icon_markdown_hollow.svg` this.classList.remove('show-markdown') break case null: case undefined: return this.classList.contains('show-markdown') } }// }}} editMarkdown(data) {// {{{ this.showMarkdown(false) this.elNodeContent.selectionStart = data.position.start this.elNodeContent.selectionEnd = data.position.end this.elNodeContent.focus() }// }}} } 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() {//{{{ 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') }//}}} 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