Notes2/static/js/node.mjs
2026-05-03 09:17:20 +02:00

229 lines
5.8 KiB
JavaScript

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 = `
<div data-el="name"></div>
<textarea data-el="node-content" required rows=1></textarea>
`
}// }}}
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