Work on sync element, now a custom HTML element

This commit is contained in:
Magnus Åhall 2026-05-03 09:17:20 +02:00
parent 99063d34be
commit 9fc4a14ce3
10 changed files with 190 additions and 1001 deletions

View file

@ -1,345 +1,20 @@
import { h, Component, createRef } from 'preact'
import htm from 'htm'
import { signal } from 'preact/signals'
import { ROOT_NODE } from 'node_store'
import { SyncProgress } from 'sync'
const html = htm.bind(h)
import { CustomHTMLElement } from './lib/custom_html_element.mjs'
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 = []
this.syncProgress = createRef()
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="sync-progress"></div>
<div id="name">${node.get('Name')}</div>
<${NodeContent} key=${node.UUID} node=${node} ref=${this.nodeContent} />
<div id="blank"></div>
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>
`
}// }}}
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() {//{{{
console.log('hum')
_notes2.current.goToNode(this.props.startNode.UUID, true)
_notes2.current.tree.expandToTrunk(this.props.startNode)
const syncProgressEl = document.getElementById('#sync-progress')
console.log(syncProgressEl)
}//}}}
setNode(node) {//{{{
this.nodeModified.value = false
this.node.value = node
}//}}}
setCrumbs(nodes) {//{{{
this.crumbs = nodes
}//}}}
async saveNode() {//{{{
if (!this.nodeModified.value)
return
/* The node history is a local store for node history.
* This could be provisioned from the server or cleared if
* deemed unnecessary.
*
* The send queue is what will be sent back to the server
* to have a recorded history of the notes.
*
* A setting to be implemented in the future could be to
* not save the history locally at all. */
const node = this.node.value
// The node is still in its old state and will present
// the unmodified content to the node store.
const history = nodeStore.nodesHistory.add(node)
// Prepares the node object for saving.
// Sets Updated value to current date and time.
await node.save()
// Updated node is added to the send queue to be stored on server.
const sendQueue = nodeStore.sendQueue.add(this.node.value)
// Updated node is saved to the primary node store.
const nodeStoreAdding = nodeStore.add([node])
await Promise.all([history, sendQueue, nodeStoreAdding])
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 'T':
if (document.activeElement.id === 'tree')
document.getElementById('node-content').focus()
else
document.getElementById('tree').focus()
break
case 'F':
_mbus.dispatch('op-search')
break
/*
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 NodeUINative {
constructor(parentElement) {// {{{
constructor() {// {{{
super()
this.node = null
this.parent = parentElement
this.parent.replaceChildren(this.createElements())
this.style.display = 'contents'
_mbus.subscribe('NODE_UI_OPEN', event => {
this.node = event.detail.data
@ -353,42 +28,25 @@ export class NodeUINative {
_mbus.subscribe('NODE_UNMODIFIED', () => {
document.querySelector('#crumbs .crumbs')?.classList.remove('node-modified')
})
}// }}}
createElements() {// {{{
const tmpl = document.createElement('template')
tmpl.innerHTML = `
<div id="name"></div>
<div class="grow-wrap" style="display: none">
<textarea id="node-content" class="node-content" required rows=1></textarea>
</div>
`
tmpl.content.querySelector('#node-content').addEventListener('input', event => this.contentChanged(event))
return tmpl.content
this.elNodeContent.addEventListener('input', event => this.contentChanged(event))
}// }}}
render() {// {{{
this.parent.querySelector('.grow-wrap').style.display = (this.node === null ? 'none' : 'grid')
this.parent.querySelector('#name').innerText = this.node?.get('Name') ?? ''
this.parent.querySelector('#node-content').value = this.node?.get('Content') ?? ''
this.resize()
return this.parent
this.elName.innerText = this.node?.get('Name') ?? ''
this.elNodeContent.value = this.node?.get('Content') ?? ''
}// }}}
takeFocus() {// {{{
this.elNodeContent.focus()
}// }}}
resize() {//{{{
const textarea = this.parent.querySelector('#node-content')
textarea.parentNode.dataset.replicatedValue = textarea.value
}//}}}
contentChanged(event) {//{{{
this.node.setContent(event.target.value)
this.resize()
}//}}}
isModified() {// {{{
return this.node?.isModified()
}// }}}
}
customElements.define('n2-nodeui', N2NodeUI)
export class Node {
static sort(a, b) {//{{{