383 lines
10 KiB
JavaScript
383 lines
10 KiB
JavaScript
import { h, Component, createRef } from 'preact'
|
|
import htm from 'htm'
|
|
import { signal } from 'preact/signals'
|
|
import { ROOT_NODE } from 'node_store'
|
|
const html = htm.bind(h)
|
|
|
|
export class NodeUI extends Component {
|
|
constructor(props) {//{{{
|
|
super(props)
|
|
this.menu = signal(false)
|
|
this.node = signal(null)
|
|
this.nodeContent = createRef()
|
|
this.nodeProperties = createRef()
|
|
this.nodeModified = signal(false)
|
|
this.keys = signal([])
|
|
this.page = signal('node')
|
|
this.crumbs = []
|
|
window.addEventListener('popstate', evt => {
|
|
if (evt.state?.hasOwnProperty('nodeUUID'))
|
|
_notes2.current.goToNode(evt.state.nodeUUID, true)
|
|
else
|
|
_notes2.current.goToNode('00000000-0000-0000-0000-000000000000', true)
|
|
})
|
|
|
|
window.addEventListener('keydown', evt => this.keyHandler(evt))
|
|
}//}}}
|
|
render() {//{{{
|
|
if (this.node.value === null)
|
|
return
|
|
|
|
const node = this.node.value
|
|
document.title = node.get('Name')
|
|
|
|
const nodeModified = this.nodeModified.value ? 'node-modified' : ''
|
|
|
|
|
|
const crumbDivs = [
|
|
html`<div class="crumb" onclick=${() => _notes2.current.goToNode(ROOT_NODE)}>Start</div>`
|
|
]
|
|
for (let i = this.crumbs.length-1; i >= 0; i--) {
|
|
const crumbNode = this.crumbs[i]
|
|
crumbDivs.push(html`<div class="crumb" onclick=${() => _notes2.current.goToNode(crumbNode.UUID)}>${crumbNode.get('Name')}</div>`)
|
|
}
|
|
if (node.UUID !== ROOT_NODE)
|
|
crumbDivs.push(
|
|
html`<div class="crumb" onclick=${() => _notes2.current.goToNode(node.UUID)}>${node.get('Name')}</div>`
|
|
)
|
|
|
|
return html`
|
|
<div id="crumbs" onclick=${() => this.saveNode()}>
|
|
<div class="crumbs ${nodeModified}">
|
|
${crumbDivs}
|
|
</div>
|
|
</div>
|
|
<div id="name">${node.get('Name')}</div>
|
|
<${NodeContent} key=${node.UUID} node=${node} ref=${this.nodeContent} />
|
|
<div id="blank"></div>
|
|
`
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
let crumbs = [
|
|
html`<div class="crumb" onclick=${() => this.goToNode(0)}>Start</div>`
|
|
]
|
|
|
|
crumbs = crumbs.concat(node.Crumbs.slice(0).map(node =>
|
|
html`<div class="crumb" onclick=${() => this.goToNode(node.ID)}>${node.Name}</div>`
|
|
).reverse())
|
|
|
|
|
|
// Page to display
|
|
let page = ''
|
|
switch (this.page.value) {
|
|
case 'node':
|
|
if (node.ID === 0) {
|
|
page = html`
|
|
<div style="cursor: pointer; color: #000; text-align: center;" onclick=${() => { this.page.value = 'schedule-events' }}>Schedule events</div>
|
|
${children.length > 0 ? html`<div class="child-nodes">${children}</div><div id="notes-version">Notes version ${window._VERSION}</div>` : html``}
|
|
`
|
|
} else {
|
|
let padlock = ''
|
|
if (node.CryptoKeyID > 0)
|
|
padlock = html`<img src="/images/${window._VERSION}/padlock-black.svg" style="height: 24px;" />`
|
|
|
|
page = html`
|
|
${children.length > 0 ? html`<div class="child-nodes">${children}</div>` : html``}
|
|
<div class="node-name">
|
|
${node.Name} ${padlock}
|
|
</div>
|
|
<${NodeContent} key=${node.ID} node=${node} ref=${this.nodeContent} />
|
|
<${NodeEvents} events=${node.ScheduleEvents.value} />
|
|
<${Checklist} ui=${this} groups=${node.ChecklistGroups} />
|
|
<${NodeFiles} node=${this.node.value} />
|
|
`
|
|
}
|
|
break
|
|
|
|
case 'upload':
|
|
page = html`<${UploadUI} nodeui=${this} />`
|
|
break
|
|
|
|
case 'node-properties':
|
|
page = html`<${NodeProperties} ref=${this.nodeProperties} nodeui=${this} />`
|
|
break
|
|
|
|
case 'keys':
|
|
page = html`<${Keys} nodeui=${this} />`
|
|
break
|
|
|
|
case 'profile-settings':
|
|
page = html`<${ProfileSettings} nodeui=${this} />`
|
|
break
|
|
|
|
case 'search':
|
|
page = html`<${Search} nodeui=${this} />`
|
|
break
|
|
|
|
case 'schedule-events':
|
|
page = html`<${ScheduleEventList} nodeui=${this} />`
|
|
break
|
|
}
|
|
|
|
const menu = () => (this.menu.value ? html`<${Menu} nodeui=${this} />` : null)
|
|
const checklist = () =>
|
|
html`
|
|
<div class="checklist" onclick=${evt => { evt.stopPropagation(); this.toggleChecklist() }}>
|
|
<img src="/images/${window._VERSION}/${this.showChecklist() ? 'checklist-on.svg' : 'checklist-off.svg'}" />
|
|
</div>`
|
|
|
|
return html`
|
|
<${menu} />
|
|
<!--header class="${modified}" onclick=${() => this.saveNode()}>
|
|
<div class="tree"><img src="/images/${window._VERSION}/tree.svg" onclick=${() => document.getElementById('app').classList.toggle('toggle-tree')} /></div>
|
|
<div class="name">Notes</div>
|
|
<div class="markdown" onclick=${evt => { evt.stopPropagation(); this.toggleMarkdown() }}><img src="/images/${window._VERSION}/${node.RenderMarkdown.value ? 'markdown.svg' : 'markdown-hollow.svg'}" /></div>
|
|
<${checklist} />
|
|
<div class="search" onclick=${evt => { evt.stopPropagation(); this.showPage('search') }}><img src="/images/${window._VERSION}/search.svg" /></div>
|
|
<div class="add" onclick=${evt => this.createNode(evt)}><img src="/images/${window._VERSION}/add.svg" /></div>
|
|
<div class="keys" onclick=${evt => { evt.stopPropagation(); this.showPage('keys') }}><img src="/images/${window._VERSION}/padlock.svg" /></div>
|
|
<div class="menu" onclick=${evt => this.showMenu(evt)}>☰</div>
|
|
</header-->
|
|
|
|
<div style="display: grid; justify-items: center;">
|
|
<div id="crumbs">
|
|
<div class="crumbs">${crumbs}</crumbs>
|
|
</div>
|
|
</div>
|
|
|
|
${page}
|
|
`
|
|
}//}}}
|
|
async componentDidMount() {//{{{
|
|
_notes2.current.goToNode(this.props.startNode.UUID, true)
|
|
_notes2.current.tree.expandToTrunk(this.props.startNode)
|
|
}//}}}
|
|
setNode(node) {//{{{
|
|
this.nodeModified.value = false
|
|
this.node.value = node
|
|
}//}}}
|
|
setCrumbs(nodes) {//{{{
|
|
this.crumbs = nodes
|
|
}//}}}
|
|
async saveNode() {//{{{
|
|
if (!this.nodeModified.value)
|
|
return
|
|
|
|
await nodeStore.copyToNodesHistory(this.node.value)
|
|
|
|
// Prepares the node object for saving.
|
|
// Sets Updated value to current date and time.
|
|
const node = this.node.value
|
|
node.save()
|
|
await nodeStore.add([node])
|
|
this.nodeModified.value = false
|
|
}//}}}
|
|
|
|
keyHandler(evt) {//{{{
|
|
let handled = true
|
|
|
|
// All keybindings is Alt+Shift, since the popular browsers at the time (2023) allows to override thees.
|
|
// Ctrl+S is the exception to using Alt+Shift, since it is overridable and in such widespread use for saving.
|
|
// Thus, the exception is acceptable to consequent use of alt+shift.
|
|
if (!(evt.shiftKey && evt.altKey) && !(evt.key.toUpperCase() == 'S' && evt.ctrlKey))
|
|
return
|
|
|
|
switch (evt.key.toUpperCase()) {
|
|
/*
|
|
case 'C':
|
|
this.showPage('node')
|
|
break
|
|
|
|
case 'E':
|
|
this.showPage('keys')
|
|
break
|
|
|
|
case 'M':
|
|
this.toggleMarkdown()
|
|
break
|
|
|
|
case 'N':
|
|
this.createNode()
|
|
break
|
|
|
|
case 'P':
|
|
this.showPage('node-properties')
|
|
break
|
|
|
|
*/
|
|
case 'S':
|
|
if (this.page.value === 'node')
|
|
this.saveNode()
|
|
else if (this.page.value === 'node-properties')
|
|
this.nodeProperties.current.save()
|
|
break
|
|
/*
|
|
|
|
case 'U':
|
|
this.showPage('upload')
|
|
break
|
|
|
|
case 'F':
|
|
this.showPage('search')
|
|
break
|
|
*/
|
|
|
|
default:
|
|
handled = false
|
|
}
|
|
|
|
if (handled) {
|
|
evt.preventDefault()
|
|
evt.stopPropagation()
|
|
}
|
|
}//}}}
|
|
|
|
}
|
|
|
|
class NodeContent extends Component {
|
|
constructor(props) {//{{{
|
|
super(props)
|
|
this.contentDiv = createRef()
|
|
this.state = {
|
|
modified: false,
|
|
}
|
|
}//}}}
|
|
render({ node }) {//{{{
|
|
let content = ''
|
|
try {
|
|
content = node.content()
|
|
} catch (err) {
|
|
return html`
|
|
<div id="node-content" class="node-content encrypted">${err.message}</div>
|
|
`
|
|
}
|
|
|
|
/*
|
|
let element
|
|
if (node.RenderMarkdown.value)
|
|
element = html`<${MarkdownContent} key='markdown-content' content=${content} />`
|
|
else
|
|
*/
|
|
const element = html`
|
|
<div class="grow-wrap">
|
|
<textarea id="node-content" class="node-content" ref=${this.contentDiv} oninput=${evt => this.contentChanged(evt)} required rows=1>${content}</textarea>
|
|
</div>
|
|
`
|
|
|
|
return element
|
|
}//}}}
|
|
componentDidMount() {//{{{
|
|
this.resize()
|
|
window.addEventListener('resize', () => this.resize())
|
|
|
|
const contentResizeObserver = new ResizeObserver(entries => {
|
|
for (const idx in entries) {
|
|
const w = entries[idx].contentRect.width
|
|
document.querySelector('#crumbs .crumbs').style.width = `${w}px`
|
|
}
|
|
});
|
|
|
|
const nodeContent = document.getElementById('node-content')
|
|
contentResizeObserver.observe(nodeContent);
|
|
|
|
}//}}}
|
|
componentDidUpdate() {//{{{
|
|
this.resize()
|
|
}//}}}
|
|
contentChanged(evt) {//{{{
|
|
_notes2.current.nodeUI.current.nodeModified.value = true
|
|
this.props.node.setContent(evt.target.value)
|
|
this.resize()
|
|
}//}}}
|
|
resize() {//{{{
|
|
const textarea = document.getElementById('node-content')
|
|
if (textarea)
|
|
textarea.parentNode.dataset.replicatedValue = textarea.value
|
|
}//}}}
|
|
}
|
|
|
|
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
|
|
}//}}}
|
|
constructor(nodeData, level) {//{{{
|
|
this.Level = level
|
|
this.data = nodeData
|
|
|
|
this.UUID = nodeData.UUID
|
|
this.ParentUUID = nodeData.ParentUUID
|
|
|
|
this._children_fetched = false
|
|
this.Children = []
|
|
|
|
this._content = this.data.Content
|
|
this._modified = false
|
|
|
|
/*
|
|
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.
|
|
*/
|
|
}//}}}
|
|
|
|
get(prop) {//{{{
|
|
return this.data[prop]
|
|
}//}}}
|
|
updated() {//{{{
|
|
// '2024-12-17T17:33:48.85939Z
|
|
return new Date(Date.parse(this.data.Updated))
|
|
}//}}}
|
|
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
|
|
return this.Children
|
|
}//}}}
|
|
content() {//{{{
|
|
/* TODO - implement crypto
|
|
if (this.CryptoKeyID != 0 && !this._decrypted)
|
|
this.#decrypt()
|
|
*/
|
|
this.modified = true
|
|
return this._content
|
|
}//}}}
|
|
|
|
setContent(new_content) {//{{{
|
|
this._content = new_content
|
|
/* 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
|
|
*/
|
|
}//}}}
|
|
save() {//{{{
|
|
this.data.Content = this._content
|
|
this.data.Updated = new Date().toISOString()
|
|
this._modified = false
|
|
}//}}}
|
|
}
|
|
|
|
// vim: foldmethod=marker
|