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

@ -25,7 +25,7 @@ import (
const VERSION = "v1"
const CONTEXT_USER = 1
const SYNC_PAGINATION = 500
const SYNC_PAGINATION = 200
var (
FlagGenerate bool

View file

@ -1,5 +1,9 @@
@import "theme.css";
:root {
--content-width: 900px;
}
html {
background-color: #fff;
}
@ -10,19 +14,18 @@ html {
display: grid;
grid-template-areas:
"tree crumbs"
"tree sync"
"tree name"
"tree sync"
"tree content"
/*
"tree checklist"
"tree schedule"
"tree files"
*/
"tree blank"
;
grid-template-columns: min-content 1fr;
grid-template-rows:
48px 56px 48px min-content 1fr;
min-content min-content 48px 1fr;
@media only screen and (max-width: 600px) {
@ -33,7 +36,6 @@ html {
"content"
/*
"checklist"
"schedule"
"files"
*/
"blank"
@ -43,8 +45,15 @@ html {
#tree {
display: none;
}
n2-syncprogress {
.el-count {
top: 4px;
}
}
}
}
#tree {
grid-area: tree;
@ -141,9 +150,10 @@ html {
display: grid;
align-items: start;
justify-items: center;
height: min-content;
margin: 16px 16px;
.crumbs {
n2-crumbs {
background: #e4e4e4;
display: flex;
flex-wrap: wrap;
@ -162,7 +172,7 @@ html {
}
}
.crumb {
n2-crumb {
margin-right: 8px;
cursor: pointer;
user-select: none;
@ -174,17 +184,17 @@ html {
}
}
.crumb:after {
content: "";
margin-left: 8px;
n2-crumb:after {
content: ">";
font-weight: bold;
color: var(--color1)
}
.crumb:last-child {
n2-crumb:last-child {
margin-right: 0;
}
.crumb:last-child:after {
n2-crumb:last-child:after {
content: '';
margin-left: 0px;
}
@ -193,31 +203,35 @@ html {
}
#sync-progress {
n2-syncprogress {
--radius: 8px;
display: grid;
grid-area: sync;
display: grid;
justify-items: center;
align-items: center;
width: 100%;
height: 56px;
.container {
position: relative;
opacity: 0;
transition: height 0s 500ms, opacity 500ms linear, visibility 0s 500ms;
&.show {
opacity: 1;
transition: visibility, height 0s, opacity 500ms linear;
}
progress {
width: 900px;
padding: 0 7px;
max-width: 900px;
width: calc(100% - 32px);
max-width: var(--content-width);
height: 24px;
border-radius: 8px;
}
.count {
position: absolute;
top: 5px;
top: 16px;
width: 100%;
white-space: nowrap;
color: #888;
@ -249,89 +263,54 @@ html {
background: linear-gradient(180deg, rgba(186, 95, 89, 1) 0%, rgba(254, 95, 85, 1) 50%, rgba(186, 95, 89, 1) 100%);
border-radius: var(--radius);
}
}
&.hidden {
visibility: hidden;
opacity: 0;
transition: visibility 0s 500ms, opacity 500ms linear;
}
}
#name {
color: #333;
font-weight: bold;
text-align: center;
font-size: 1.15em;
margin-top: 0px;
margin-bottom: 16px;
}
/* ============================================================= *
* Textarea replicates the height of an element expanding height *
* ============================================================= */
.grow-wrap {
/* easy way to plop the elements on top of each other and have them both sized based on the tallest one's height */
display: grid;
grid-area: content;
font-size: 1.0em;
}
.grow-wrap::after {
/* Note the weird space! Needed to preventy jumpy behavior */
content: attr(data-replicated-value) " ";
/* This is how textarea text behaves */
width: calc(100% - 32px);
max-width: 900px;
white-space: pre-wrap;
word-wrap: break-word;
background: rgba(0, 255, 255, 0.5);
justify-self: center;
/* Hidden from view, clicks, and screen readers */
visibility: hidden;
}
.grow-wrap>textarea {
/* You could leave this, but after a user resizes, then it ruins the auto sizing */
resize: none;
/* Firefox shows scrollbar on growth, you can hide like this. */
overflow: hidden;
}
.grow-wrap>textarea,
.grow-wrap::after {
/* Identical styling required!! */
padding: 0.5rem;
font: inherit;
/* Place on top of each other */
grid-area: 1 / 1 / 2 / 2;
}
/* ============================================================= */
#node-content {
n2-nodeui {
margin-bottom: 32px;
.el-name {
color: #333;
font-weight: bold;
text-align: center;
font-size: 1.15em;
margin-top: 8px;
margin-bottom: 0px;
}
.el-node-content {
justify-self: center;
word-wrap: break-word;
font-family: monospace;
color: #333;
/*
width: 100%;
max-width: var(--content-width);
field-sizing: content;
*/
width: calc(100% - 32px);
max-width: 900px;
max-width: var(--content-width);
field-sizing: content;
resize: none;
border: none;
outline: none;
padding: 16px 0;
border-top: 1px solid #e0e0e0;
border-bottom: 1px solid #e0e0e0;
margin-bottom: 32px;
&:invalid {
background: #f5f5f5;
padding-top: 16px;
}
}
}
#blank {
grid-area: blank;

View file

@ -1,8 +1,7 @@
import { ROOT_NODE } from 'node_store'
import { CustomHTMLElement } from './lib/custom_html_element.mjs'
import { N2Tree } from 'tree'
import { NodeUINative, Node } from 'node'
import { SyncProgress } from 'sync'
import { Node } from 'node'
export class App {
constructor() {// {{{
@ -10,7 +9,7 @@ export class App {
this.treeNative = new N2Tree()
this.crumbs = new N2Crumbs()
this.crumbsElement = document.getElementById('crumbs')
this.nodeUI = new NodeUINative(document.getElementById('note'))
this.nodeUI = document.getElementById('note')
_mbus.subscribe('TREE_TRUNK_FETCHED', async () => {
document.getElementById('tree').append(this.treeNative.render())
@ -36,11 +35,12 @@ export class App {
document.getElementById('node-content')?.focus()
})
const syncProgress = document.getElementById('sync-progress')
new SyncProgress(syncProgress)
window._sync = new Sync()
window._sync.run()
// I think it is uncomfortable having the sync running as soon as the page load.
// I haven't gotten the time to look at the page before stuff jumps around.
// There a slight delay to initiate sync seems reasonable.
setTimeout(() => window._sync.run(), 1000)
}// }}}
keyHandler(event) {//{{{
@ -55,9 +55,9 @@ export class App {
switch (event.key.toUpperCase()) {
case 'T':
if (document.activeElement.id === 'tree-nodes')
document.getElementById('node-content').focus()
this.nodeUI.takeFocus()
else
document.getElementById('tree-nodes').focus()
this.nodeUI.takeFocus()
break
case 'F':

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>
`
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}
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>
`
}// }}}
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) {//{{{

View file

@ -13,7 +13,7 @@ export class NodeStore {
this.sendQueue = null
this.nodesHistory = null
}//}}}
async initializeDB() {//{{{
initializeDB() {//{{{
return new Promise((resolve, reject) => {
const req = indexedDB.open('notes', 7)
@ -78,7 +78,7 @@ export class NodeStore {
}
})
}//}}}
async initializeRootNode() {//{{{
initializeRootNode() {//{{{
return new Promise((resolve, reject) => {
// The root node is a magical node which displays as the first node if none is specified.
// If not already existing, it will be created.
@ -120,7 +120,7 @@ export class NodeStore {
return n
}//}}}
async getAppState(key) {//{{{
getAppState(key) {//{{{
return new Promise((resolve, reject) => {
const trx = this.db.transaction('app_state', 'readonly')
const appState = trx.objectStore('app_state')
@ -135,7 +135,7 @@ export class NodeStore {
getRequest.onerror = (event) => reject(event.target.error)
})
}//}}}
async setAppState(key, value) {//{{{
setAppState(key, value) {//{{{
return new Promise((resolve, reject) => {
try {
const t = this.db.transaction('app_state', 'readwrite')
@ -159,32 +159,7 @@ export class NodeStore {
})
}//}}}
/* TODO - Remove?
async storeNode(node) {//{{{
return new Promise((resolve, reject) => {
const t = this.db.transaction('nodes', 'readwrite')
const nodeStore = t.objectStore('nodes')
t.onerror = (event) => {
console.log('transaction error', event.target.error)
reject(event.target.error)
}
t.oncomplete = () => {
resolve()
}
const nodeReq = nodeStore.put(node.data)
nodeReq.onsuccess = () => {
console.debug(`Storing ${node.UUID} (${node.get('Name')})`)
}
queueReq.onerror = (event) => {
console.log(`Error storing ${node.UUID}`, event.target.error)
reject(event.target.error)
}
})
}//}}}
*/
async upsertNodeRecords(records) {//{{{
upsertNodeRecords(records) {//{{{
return new Promise((resolve, reject) => {
const t = this.db.transaction('nodes', 'readwrite')
const nodeStore = t.objectStore('nodes')
@ -222,7 +197,7 @@ export class NodeStore {
}
})
}//}}}
async getTreeNodes(parent, newLevel) {//{{{
getTreeNodes(parent, newLevel) {//{{{
return new Promise((resolve, reject) => {
// Parent of toplevel nodes is ROOT_NODE in indexedDB.
// Only the root node has '' as parent.
@ -274,13 +249,13 @@ export class NodeStore {
})
}//}}}
async add(records) {//{{{
add(records) {//{{{
return new Promise((resolve, reject) => {
try {
const t = this.db.transaction('nodes', 'readwrite')
const nodeStore = t.objectStore('nodes')
t.onerror = (event) => {
console.log('transaction error', event.target.error)
console.error('transaction error', event.target.error)
reject(event.target.error)
}
@ -291,12 +266,9 @@ export class NodeStore {
const addReq = nodeStore.put(record.data)
const promise = new Promise((resolve, reject) => {
addReq.onsuccess = () => {
console.debug('OK!', record.ID, record.Name)
resolve()
}
addReq.onsuccess = () => resolve()
addReq.onerror = (event) => {
console.log('Error!', event.target.error, record.ID)
console.error('Error!', event.target.error, record.ID)
reject(event.target.error)
}
})
@ -309,7 +281,7 @@ export class NodeStore {
}
})
}//}}}
async get(uuid) {//{{{
get(uuid) {//{{{
return new Promise((resolve, reject) => {
const trx = this.db.transaction('nodes', 'readonly')
const nodeStore = trx.objectStore('nodes')
@ -325,7 +297,7 @@ export class NodeStore {
}
})
}//}}}
async getNodeAncestry(node, accumulated) {//{{{
getNodeAncestry(node, accumulated) {//{{{
return new Promise((resolve, reject) => {
if (accumulated === undefined)
accumulated = []
@ -357,7 +329,7 @@ export class NodeStore {
}//}}}
async nodeCount() {//{{{
nodeCount() {//{{{
return new Promise((resolve, reject) => {
const t = this.db.transaction('nodes', 'readwrite')
const nodeStore = t.objectStore('nodes')
@ -374,7 +346,7 @@ class SimpleNodeStore {
this.db = db
this.storeName = storeName
}//}}}
async add(node) {//{{{
add(node) {//{{{
return new Promise((resolve, reject) => {
const t = this.db.transaction(['nodes', this.storeName], 'readwrite')
const store = t.objectStore(this.storeName)
@ -394,7 +366,7 @@ class SimpleNodeStore {
}
})
}//}}}
async retrieve(limit) {//{{{
retrieve(limit) {//{{{
return new Promise((resolve, reject) => {
const cursorReq = this.db
.transaction(['nodes', this.storeName], 'readonly')
@ -422,7 +394,7 @@ class SimpleNodeStore {
}
})
}//}}}
async delete(keys) {//{{{
delete(keys) {//{{{
const store = this.db
.transaction(['nodes', this.storeName], 'readwrite')
.objectStore(this.storeName)
@ -439,7 +411,7 @@ class SimpleNodeStore {
}
return Promise.all(promises)
}//}}}
async count() {//{{{
count() {//{{{
const store = this.db
.transaction(['nodes', this.storeName], 'readonly')
.objectStore(this.storeName)

View file

@ -1,5 +1,6 @@
import { API } from 'api'
import { Node } from 'node'
import { CustomHTMLElement } from './lib/custom_html_element.mjs'
export class Sync {
constructor() {//{{{
@ -154,70 +155,43 @@ export class Sync {
}//}}}
}
export class SyncProgress {
constructor(parentEl) {//{{{
export class N2SyncProgress extends CustomHTMLElement {
static {
this.tmpl = document.createElement('template')
this.tmpl.innerHTML = `
<progress data-el="progress" min=0 max=137 value=0></progress>
<div data-el="count" class="count">0 / 0</div>
`
}
constructor() {//{{{
super()
this.reset()
_mbus.subscribe('SYNC_COUNT', event => this.progressHandler(event))
_mbus.subscribe('SYNC_HANDLED', event => this.progressHandler(event))
_mbus.subscribe('SYNC_DONE', event => this.progressHandler(event))
this.el = this.createElements()
parentEl.replaceChildren(this.el)
}//}}}
createElements() {
const div = document.createElement('div')
div.classList.add('container')
div.innerHTML = `
<progress min=0 max=137 value=0></progress>
<div class="count">0 / 0</div>
`
this.elProgress = div.querySelector('progress')
this.elCount = div.querySelector('.count')
return div
}
reset() {//{{{
this.forceUpdateRequest = null
this.state = {
nodesToSync: 0,
nodesSynced: 0,
syncedDone: false,
}
document.getElementById('sync-progress')?.classList.remove('hidden')
}//}}}
getSnapshotBeforeUpdate(_, prevState) {//{{{
if (!prevState.syncedDone && this.state.syncedDone)
setTimeout(() => document.getElementById('sync-progress')?.classList.add('hidden'), 750)
}//}}}
componentDidUpdate() {//{{{
if (!this.state.syncedDone) {
if (this.forceUpdateRequest !== null)
clearTimeout(this.forceUpdateRequest)
this.forceUpdateRequest = setTimeout(
() => {
this.forceUpdateRequest = null
this.forceUpdate()
},
50
)
}
}//}}}
progressHandler(event) {//{{{
const eventData = event.detail.data
switch (event.type) {
case 'SYNC_COUNT':
console.log(eventData.count)
this.state.nodesToSync = eventData.count
this.setSyncState(true)
break
case 'SYNC_HANDLED':
console.log('sync handled')
this.state.nodesSynced = eventData.handled
break
case 'SYNC_DONE':
// Hides the progress bar.
this.state.syncedDone = true
this.setSyncState(false)
// Don't update anything if nothing was synced.
if (this.state.nodesSynced === 0)
@ -233,17 +207,17 @@ export class SyncProgress {
this.render()
}//}}}
render() {//{{{
console.log('render', this.state.nodesToSync)
this.elProgress.max = this.state.nodesToSync
this.elProgress.value = this.state.nodesSynced
this.elCount.innerText = `${this.state.nodesSynced} / ${this.state.nodesToSync}`
/*
if (nodesToSync === 0)
return html`<div id="sync-progress"></div>`
*/
}//}}}
setSyncState(state) {// {{{
if (state)
this.classList.add('show')
else
setTimeout(() => this.classList.remove('show'), 1500)
}// }}}
}
customElements.define('n2-syncprogress', N2SyncProgress)
// vim: foldmethod=marker

View file

@ -1,365 +0,0 @@
@import "theme.less";
html {
background-color: #fff;
}
#notes2 {
min-height: 100vh;
display: grid;
grid-template-areas:
"tree crumbs"
"tree sync"
"tree name"
"tree content"
//"tree checklist"
//"tree schedule"
//"tree files"
"tree blank"
;
grid-template-columns: min-content 1fr;
grid-template-rows:
48px
56px
48px
min-content
1fr;
@media only screen and (max-width: 600px) {
grid-template-areas:
"crumbs"
"sync"
"name"
"content"
//"checklist"
//"schedule"
//"files"
"blank"
;
grid-template-columns: 1fr;
#tree {
display: none;
}
}
}
#tree {
grid-area: tree;
display: grid;
padding: 16px 0px 16px 16px;
color: #ddd;
z-index: 100; // Over crumbs shadow
border-left: 2px solid #333;
&:focus {
border-left: 2px solid #FE5F55;
}
#logo {
display: grid;
position: relative;
justify-items: center;
margin-top: 8px;
margin-bottom: 8px;
margin-left: 24px;
margin-right: 24px;
cursor: pointer;
img {
width: 128px;
left: -20px;
}
}
.icons {
display: flex;
justify-content: center;
margin-bottom: 32px;
gap: 8px;
}
.node {
display: grid;
grid-template-columns: 24px min-content;
grid-template-rows:
min-content
1fr;
margin-top: 12px;
.expand-toggle {
user-select: none;
img {
width: 16px;
height: 16px;
}
}
.name {
white-space: nowrap;
cursor: pointer;
user-select: none;
&:hover {
color: @color1;
}
&.selected {
color: @color1;
font-weight: bold;
}
}
.children {
padding-left: 24px;
margin-left: 8px;
border-left: 1px solid #444;
grid-column: 1 / -1;
&.collapsed {
display: none;
}
}
}
}
#tree-nodes {
padding: 16px 32px;
background-color: #333;
border-radius: 8px;
box-shadow: 5px 5px 10px -5px rgba(0,0,0,0.75);
}
#crumbs {
grid-area: crumbs;
display: grid;
align-items: start;
justify-items: center;
margin: 16px 16px;
.crumbs {
background: #e4e4e4;
display: flex;
flex-wrap: wrap;
padding: 8px 16px;
background: #e4e4e4;
color: #333;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
&.node-modified {
background-color: @color1;
color: @color2;
.crumb:after {
color: @color2;
}
}
.crumb {
margin-right: 8px;
cursor: pointer;
user-select: none;
-webkit-tap-highlight-color: transparent;
a {
text-decoration: none;
color: inherit;
}
}
.crumb:after {
content: "•";
margin-left: 8px;
color: @color1
}
.crumb:last-child {
margin-right: 0;
}
.crumb:last-child:after {
content: '';
margin-left: 0px;
}
}
}
#sync-progress {
grid-area: sync;
display: grid;
justify-items: center;
width: 100%;
height: 56px;
position: relative;
progress {
width: 100%;
padding: 0 7px;
max-width: 900px;
height: 16px;
border-radius: 4px;
}
progress[value]::-webkit-progress-bar {
background-color: #eee;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25) inset;
border-radius: 4px;
}
progress[value]::-moz-progress-bar {
background-color: #eee;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25) inset;
border-radius: 4px;
}
progress[value]::-webkit-progress-value {
background: rgb(186,95,89);
background: linear-gradient(180deg, rgba(186,95,89,1) 0%, rgba(254,95,85,1) 50%, rgba(186,95,89,1) 100%);
border-radius: 4px;
}
// TODO: style the progress value for Firefox
progress[value]::-moz-progress-value {
background: rgb(186,95,89);
background: linear-gradient(180deg, rgba(186,95,89,1) 0%, rgba(254,95,85,1) 50%, rgba(186,95,89,1) 100%);
border-radius: 4px;
}
.count {
width: min-content;
white-space: nowrap;
margin-top: 0px;
color: #888;
position: absolute;
top: 22px;
}
&.hidden {
visibility: hidden;
opacity: 0;
transition: visibility 0s 500ms, opacity 500ms linear;
}
}
#name {
color: #333;
font-weight: bold;
text-align: center;
font-size: 1.15em;
margin-top: 0px;
margin-bottom: 16px;
}
/* ============================================================= *
* Textarea replicates the height of an element expanding height *
* ============================================================= */
.grow-wrap {
/* easy way to plop the elements on top of each other and have them both sized based on the tallest one's height */
display: grid;
grid-area: content;
font-size: 1.0em;
}
.grow-wrap::after {
/* Note the weird space! Needed to preventy jumpy behavior */
content: attr(data-replicated-value) " ";
/* This is how textarea text behaves */
width: calc(100% - 32px);
max-width: 900px;
white-space: pre-wrap;
word-wrap: break-word;
background: rgba(0, 255, 255, 0.5);
justify-self: center;
/* Hidden from view, clicks, and screen readers */
visibility: hidden;
}
.grow-wrap > textarea {
/* You could leave this, but after a user resizes, then it ruins the auto sizing */
resize: none;
/* Firefox shows scrollbar on growth, you can hide like this. */
overflow: hidden;
}
.grow-wrap > textarea,
.grow-wrap::after {
/* Identical styling required!! */
padding: 0.5rem;
font: inherit;
/* Place on top of each other */
grid-area: 1 / 1 / 2 / 2;
}
/* ============================================================= */
#node-content {
justify-self: center;
word-wrap: break-word;
font-family: monospace;
color: #333;
width: calc(100% - 32px);
max-width: 900px;
resize: none;
border: none;
outline: none;
&:invalid {
background: #f5f5f5;
padding-top: 16px;
}
}
#blank {
grid-area: blank;
height: 32px;
}
dialog.op {
&::backdrop {
background: rgba(0, 0, 0, 0.5);
}
.header {
font-weight: bold;
margin-top: 16px;
&:first-child {
margin-top: 0px;
}
}
}
#op-search {
.results {
display: grid;
grid-template-columns: min-content min-content;
grid-gap: 6px 16px;
div {
white-space: nowrap;
}
.ancestors {
display: flex;
.ancestor::after {
content: ">";
margin: 0px 8px;
color: #a00;
}
.ancestor:last-child::after {
content: "";
}
}
}
}

View file

@ -6,13 +6,6 @@ const CACHED_ASSETS = [
'/css/{{ .VERSION }}/main.css',
'/css/{{ .VERSION }}/notes2.css',
'/js/{{ .VERSION }}/lib/preact/preact.mjs',
'/js/{{ .VERSION }}/lib/preact/devtools.mjs',
'/js/{{ .VERSION }}/lib/signals/signals.mjs',
'/js/{{ .VERSION }}/lib/signals/signals-core.mjs',
'/js/{{ .VERSION }}/lib/preact/hooks.mjs',
'/js/{{ .VERSION }}/lib/preact/debug.mjs',
'/js/{{ .VERSION }}/lib/htm/htm.mjs',
'/js/{{ .VERSION }}/lib/fullcalendar.min.js',
'/js/{{ .VERSION }}/lib/node_modules/marked/marked.min.js',
'/js/{{ .VERSION }}/lib/sjcl.js',

View file

@ -2,21 +2,11 @@
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="initial-scale=1.0, user-scalable=yes" />
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/main.css">
<script type="importmap">
{
"imports": {
"preact": "/js/{{ .VERSION }}/lib/preact/preact.mjs",
"preact/hooks": "/js/{{ .VERSION }}/lib/preact/hooks.mjs",
{{- if .Data._dev }}
"preact/debug": "/js/{{ .VERSION }}/lib/preact/debug.mjs",
"preact/devtools": "/js/{{ .VERSION }}/lib/preact/devtools.mjs",
{{- end }}
"@preact/signals-core": "/js/{{ .VERSION }}/lib/signals/signals-core.mjs",
"preact/signals": "/js/{{ .VERSION }}/lib/signals/signals.mjs",
"htm": "/js/{{ .VERSION }}/lib/htm/htm.mjs",
"api": "/js/{{ .VERSION }}/api.mjs",
"sync": "/js/{{ .VERSION }}/sync.mjs",
"key": "/js/{{ .VERSION }}/key.mjs",

View file

@ -2,24 +2,13 @@
<div id="notes2">
<div id="tree" tabindex=0></div>
<div id="crumbs"></div>
<div id="sync-progress">
<progress min=0 max=1 value=0></progress>
<div class="count">0 / 1</div>
</div>
<div id="note"></div>
<!--div id="blank"></div-->
<n2-syncprogress></n2-syncprogress>
<n2-nodeui id="note"></n2-nodeui>
</div>
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/notes2.css">
<script type="module">
import { h, Component, render, createRef } from 'preact'
import htm from 'htm'
{{ if .Data._dev -}}
import 'preact/debug'
import 'preact/devtools'
{{- end }}
import {NodeStore} from 'node_store'
import {App} from "/js/{{ .VERSION }}/app.mjs"
import {API} from 'api'
@ -30,7 +19,6 @@ window.Sync = Sync
if (!API.hasAuthenticationToken()) {
location.href = '/login'
} else {
const html = htm.bind(h)
try {
window.nodeStore = new NodeStore()
window.nodeStore.initializeDB().then(() => {