229 lines
5.8 KiB
JavaScript
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
|