diff --git a/main.go b/main.go
index 96f08aa..e608601 100644
--- a/main.go
+++ b/main.go
@@ -25,7 +25,7 @@ import (
const VERSION = "v1"
const CONTEXT_USER = 1
-const SYNC_PAGINATION = 200
+const SYNC_PAGINATION = 500
var (
FlagGenerate bool
diff --git a/static/css/notes2.css b/static/css/notes2.css
index e6d0ef6..adfa51d 100644
--- a/static/css/notes2.css
+++ b/static/css/notes2.css
@@ -1,9 +1,5 @@
@import "theme.css";
-:root {
- --content-width: 900px;
-}
-
html {
background-color: #fff;
}
@@ -14,18 +10,19 @@ html {
display: grid;
grid-template-areas:
"tree crumbs"
- "tree name"
"tree sync"
+ "tree name"
"tree content"
/*
"tree checklist"
+ "tree schedule"
"tree files"
*/
"tree blank"
;
grid-template-columns: min-content 1fr;
grid-template-rows:
- min-content min-content 48px 1fr;
+ 48px 56px 48px min-content 1fr;
@media only screen and (max-width: 600px) {
@@ -36,6 +33,7 @@ html {
"content"
/*
"checklist"
+ "schedule"
"files"
*/
"blank"
@@ -45,14 +43,7 @@ html {
#tree {
display: none;
}
-
- n2-syncprogress {
- .el-count {
- top: 4px;
- }
- }
}
-
}
#tree {
@@ -150,10 +141,9 @@ html {
display: grid;
align-items: start;
justify-items: center;
- height: min-content;
margin: 16px 16px;
- n2-crumbs {
+ .crumbs {
background: #e4e4e4;
display: flex;
flex-wrap: wrap;
@@ -172,7 +162,7 @@ html {
}
}
- n2-crumb {
+ .crumb {
margin-right: 8px;
cursor: pointer;
user-select: none;
@@ -184,17 +174,17 @@ html {
}
}
- n2-crumb:after {
- content: ">";
- font-weight: bold;
+ .crumb:after {
+ content: "•";
+ margin-left: 8px;
color: var(--color1)
}
- n2-crumb:last-child {
+ .crumb:last-child {
margin-right: 0;
}
- n2-crumb:last-child:after {
+ .crumb:last-child:after {
content: '';
margin-left: 0px;
}
@@ -203,112 +193,143 @@ html {
}
-n2-syncprogress {
+#sync-progress {
--radius: 8px;
- display: grid;
grid-area: sync;
display: grid;
justify-items: center;
align-items: center;
- position: relative;
+ width: 100%;
+ height: 56px;
- opacity: 0;
- transition: height 0s 500ms, opacity 500ms linear, visibility 0s 500ms;
+ .container {
+ position: relative;
- &.show {
- opacity: 1;
- transition: visibility, height 0s, opacity 500ms linear;
+ progress {
+ width: 900px;
+ padding: 0 7px;
+ max-width: 900px;
+ height: 24px;
+ border-radius: 8px;
+ }
+
+ .count {
+ position: absolute;
+ top: 5px;
+ width: 100%;
+ white-space: nowrap;
+ color: #888;
+ text-align: center;
+ font-size: 12pt;
+ font-weight: bold;
+ }
+
+ progress[value]::-webkit-progress-bar {
+ background-color: #eee;
+ box-shadow: 0 2px var(--radius) rgba(0, 0, 0, 0.25) inset;
+ border-radius: var(--radius);
+ }
+
+ progress[value]::-moz-progress-bar {
+ background-color: #eee;
+ box-shadow: 0 2px var(--radius) rgba(0, 0, 0, 0.25) inset;
+ border-radius: var(--radius);
+ }
+
+ 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: var(--radius);
+ }
+
+ 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: var(--radius);
+ }
}
- progress {
- width: calc(100% - 32px);
- max-width: var(--content-width);
- height: 24px;
- border-radius: 8px;
- }
- .count {
- position: absolute;
- top: 16px;
- width: 100%;
- white-space: nowrap;
- color: #888;
- text-align: center;
- font-size: 12pt;
- font-weight: bold;
- }
- progress[value]::-webkit-progress-bar {
- background-color: #eee;
- box-shadow: 0 2px var(--radius) rgba(0, 0, 0, 0.25) inset;
- border-radius: var(--radius);
- }
-
- progress[value]::-moz-progress-bar {
- background-color: #eee;
- box-shadow: 0 2px var(--radius) rgba(0, 0, 0, 0.25) inset;
- border-radius: var(--radius);
- }
-
- 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: var(--radius);
- }
-
- 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: 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;
+}
+
/* ============================================================= */
-n2-nodeui {
- margin-bottom: 32px;
+#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;
- .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: 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;
- }
+ &:invalid {
+ background: #f5f5f5;
+ padding-top: 16px;
}
}
diff --git a/static/js/app.mjs b/static/js/app.mjs
index c68475e..1e63b57 100644
--- a/static/js/app.mjs
+++ b/static/js/app.mjs
@@ -1,15 +1,15 @@
import { ROOT_NODE } from 'node_store'
-import { CustomHTMLElement } from './lib/custom_html_element.mjs'
import { N2Tree } from 'tree'
-import { Node } from 'node'
+import { NodeUINative, Node } from 'node'
+import { SyncProgress } from 'sync'
export class App {
constructor() {// {{{
this.currentNode = null
this.treeNative = new N2Tree()
- this.crumbs = new N2Crumbs()
+ this.crumbs = new Crumbs()
this.crumbsElement = document.getElementById('crumbs')
- this.nodeUI = document.getElementById('note')
+ this.nodeUI = new NodeUINative(document.getElementById('note'))
_mbus.subscribe('TREE_TRUNK_FETCHED', async () => {
document.getElementById('tree').append(this.treeNative.render())
@@ -35,12 +35,11 @@ export class App {
document.getElementById('node-content')?.focus()
})
- window._sync = new Sync()
+ const syncProgress = document.getElementById('sync-progress')
+ new SyncProgress(syncProgress)
- // 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)
+ window._sync = new Sync()
+ window._sync.run()
}// }}}
keyHandler(event) {//{{{
@@ -55,9 +54,9 @@ export class App {
switch (event.key.toUpperCase()) {
case 'T':
if (document.activeElement.id === 'tree-nodes')
- this.nodeUI.takeFocus()
+ document.getElementById('node-content').focus()
else
- this.nodeUI.takeFocus()
+ document.getElementById('tree-nodes').focus()
break
case 'F':
@@ -197,17 +196,11 @@ export class App {
}//}}}
}
-class N2Crumbs extends CustomHTMLElement {
- static {// {{{
- this.tmpl = document.createElement('template')
- this.tmpl.innerHTML = `
- `
- }// }}}
+class Crumbs {
constructor() {// {{{
- super()
- this.classList.add('crumbs')
-
this.crumbs = []
+ this.crumbsDiv = document.createElement('div')
+ this.crumbsDiv.classList.add('crumbs')
_mbus.subscribe('CRUMBS_SET', event => {
this.crumbs = event.detail.data
@@ -215,40 +208,38 @@ class N2Crumbs extends CustomHTMLElement {
}// }}}
render() {// {{{
const crumbs = this.crumbs.map(node =>
- new N2Crumb(
+ (new Crumb(
node.get('Name'),
node.UUID,
- )
+ )).render()
)
- const start = new N2Crumb('Start', ROOT_NODE)
+ const start = (new Crumb('Start', ROOT_NODE)).render()
crumbs.push(start)
- this.replaceChildren(...crumbs.reverse())
- return this
+ this.crumbsDiv.replaceChildren(...crumbs.reverse())
+ return this.crumbsDiv
}// }}}
}
-customElements.define('n2-crumbs', N2Crumbs)
-class N2Crumb extends CustomHTMLElement {
- static {// {{{
- this.tmpl = document.createElement('template')
- this.tmpl.innerHTML = `
-
- `
- }// }}}
+class Crumb {
constructor(label, uuid) {// {{{
- super()
- this.classList.add('crumb')
-
this.label = label
this.uuid = uuid
+ }// }}}
+ render() {// {{{
+ const crumb = document.createElement('div')
+ crumb.classList.add('crumb')
+
+ const link = document.createElement('a')
+ link.href = `/notes2#${this.uuid}`
+ link.innerText = this.label
+
+ crumb.appendChild(link)
+ return crumb
- this.elLink.href = `/notes2#${this.uuid}`
- this.elLink.innerText = this.label
}// }}}
}
-customElements.define('n2-crumb', N2Crumb)
function tmpl(html) {// {{{
const el = document.createElement('template')
diff --git a/static/js/node.mjs b/static/js/node.mjs
index 949724c..3820941 100644
--- a/static/js/node.mjs
+++ b/static/js/node.mjs
@@ -1,20 +1,345 @@
+import { h, Component, createRef } from 'preact'
+import htm from 'htm'
+import { signal } from 'preact/signals'
import { ROOT_NODE } from 'node_store'
-import { CustomHTMLElement } from './lib/custom_html_element.mjs'
+import { SyncProgress } from 'sync'
+const html = htm.bind(h)
-export class N2NodeUI extends CustomHTMLElement {
- static {// {{{
- this.tmpl = document.createElement('template')
- this.tmpl.innerHTML = `
-
-
+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` _notes2.current.goToNode(ROOT_NODE)}>Start
`
+ ]
+ for (let i = this.crumbs.length - 1; i >= 0; i--) {
+ const crumbNode = this.crumbs[i]
+ crumbDivs.push(html` _notes2.current.goToNode(crumbNode.UUID)}>${crumbNode.get('Name')}
`)
+ }
+ if (node.UUID !== ROOT_NODE)
+ crumbDivs.push(
+ html` _notes2.current.goToNode(node.UUID)}>${node.get('Name')}
`
+ )
+
+ return html`
+ this.saveNode()}>
+
+ ${crumbDivs}
+
+
+
+ ${node.get('Name')}
+ <${NodeContent} key=${node.UUID} node=${node} ref=${this.nodeContent} />
+
`
- }// }}}
- constructor() {// {{{
- super()
+
+
+
+ return
+
+ let crumbs = [
+ html` this.goToNode(0)}>Start
`
+ ]
+
+ crumbs = crumbs.concat(node.Crumbs.slice(0).map(node =>
+ html` this.goToNode(node.ID)}>${node.Name}
`
+ ).reverse())
+
+
+ // Page to display
+ let page = ''
+ switch (this.page.value) {
+ case 'node':
+ if (node.ID === 0) {
+ page = html`
+ { this.page.value = 'schedule-events' }}>Schedule events
+ ${children.length > 0 ? html`${children}
Notes version ${window._VERSION}
` : html``}
+ `
+ } else {
+ let padlock = ''
+ if (node.CryptoKeyID > 0)
+ padlock = html` `
+
+ page = html`
+ ${children.length > 0 ? html`${children}
` : html``}
+
+ ${node.Name} ${padlock}
+
+ <${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`
+ { evt.stopPropagation(); this.toggleChecklist() }}>
+
+
`
+
+ return html`
+ <${menu} />
+
+
+
+
+
+ ${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`
+
${err.message}
+ `
+ }
+
+ /*
+ let element
+ if (node.RenderMarkdown.value)
+ element = html`<${MarkdownContent} key='markdown-content' content=${content} />`
+ else
+ */
+ const element = html`
+
+
+
+ `
+
+ 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) {// {{{
this.node = null
-
- this.style.display = 'contents'
+ this.parent = parentElement
+ this.parent.replaceChildren(this.createElements())
_mbus.subscribe('NODE_UI_OPEN', event => {
this.node = event.detail.data
@@ -28,25 +353,42 @@ export class N2NodeUI extends CustomHTMLElement {
_mbus.subscribe('NODE_UNMODIFIED', () => {
document.querySelector('#crumbs .crumbs')?.classList.remove('node-modified')
})
+ }// }}}
+ createElements() {// {{{
+ const tmpl = document.createElement('template')
+ tmpl.innerHTML = `
+
+
+
+
+ `
- this.elNodeContent.addEventListener('input', event => this.contentChanged(event))
+ tmpl.content.querySelector('#node-content').addEventListener('input', event => this.contentChanged(event))
+
+ return tmpl.content
}// }}}
render() {// {{{
- this.elName.innerText = this.node?.get('Name') ?? ''
- this.elNodeContent.value = this.node?.get('Content') ?? ''
- }// }}}
- takeFocus() {// {{{
- this.elNodeContent.focus()
+ 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
}// }}}
+ 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) {//{{{
diff --git a/static/js/node_store.mjs b/static/js/node_store.mjs
index 98642d1..ef43233 100644
--- a/static/js/node_store.mjs
+++ b/static/js/node_store.mjs
@@ -13,7 +13,7 @@ export class NodeStore {
this.sendQueue = null
this.nodesHistory = null
}//}}}
- initializeDB() {//{{{
+ async initializeDB() {//{{{
return new Promise((resolve, reject) => {
const req = indexedDB.open('notes', 7)
@@ -78,7 +78,7 @@ export class NodeStore {
}
})
}//}}}
- initializeRootNode() {//{{{
+ async 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
}//}}}
- getAppState(key) {//{{{
+ async 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)
})
}//}}}
- setAppState(key, value) {//{{{
+ async setAppState(key, value) {//{{{
return new Promise((resolve, reject) => {
try {
const t = this.db.transaction('app_state', 'readwrite')
@@ -159,7 +159,32 @@ export class NodeStore {
})
}//}}}
- upsertNodeRecords(records) {//{{{
+ /* 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) {//{{{
return new Promise((resolve, reject) => {
const t = this.db.transaction('nodes', 'readwrite')
const nodeStore = t.objectStore('nodes')
@@ -197,7 +222,7 @@ export class NodeStore {
}
})
}//}}}
- getTreeNodes(parent, newLevel) {//{{{
+ async getTreeNodes(parent, newLevel) {//{{{
return new Promise((resolve, reject) => {
// Parent of toplevel nodes is ROOT_NODE in indexedDB.
// Only the root node has '' as parent.
@@ -249,13 +274,13 @@ export class NodeStore {
})
}//}}}
- add(records) {//{{{
+ async add(records) {//{{{
return new Promise((resolve, reject) => {
try {
const t = this.db.transaction('nodes', 'readwrite')
const nodeStore = t.objectStore('nodes')
t.onerror = (event) => {
- console.error('transaction error', event.target.error)
+ console.log('transaction error', event.target.error)
reject(event.target.error)
}
@@ -266,9 +291,12 @@ export class NodeStore {
const addReq = nodeStore.put(record.data)
const promise = new Promise((resolve, reject) => {
- addReq.onsuccess = () => resolve()
+ addReq.onsuccess = () => {
+ console.debug('OK!', record.ID, record.Name)
+ resolve()
+ }
addReq.onerror = (event) => {
- console.error('Error!', event.target.error, record.ID)
+ console.log('Error!', event.target.error, record.ID)
reject(event.target.error)
}
})
@@ -281,7 +309,7 @@ export class NodeStore {
}
})
}//}}}
- get(uuid) {//{{{
+ async get(uuid) {//{{{
return new Promise((resolve, reject) => {
const trx = this.db.transaction('nodes', 'readonly')
const nodeStore = trx.objectStore('nodes')
@@ -297,7 +325,7 @@ export class NodeStore {
}
})
}//}}}
- getNodeAncestry(node, accumulated) {//{{{
+ async getNodeAncestry(node, accumulated) {//{{{
return new Promise((resolve, reject) => {
if (accumulated === undefined)
accumulated = []
@@ -329,7 +357,7 @@ export class NodeStore {
}//}}}
- nodeCount() {//{{{
+ async nodeCount() {//{{{
return new Promise((resolve, reject) => {
const t = this.db.transaction('nodes', 'readwrite')
const nodeStore = t.objectStore('nodes')
@@ -346,7 +374,7 @@ class SimpleNodeStore {
this.db = db
this.storeName = storeName
}//}}}
- add(node) {//{{{
+ async add(node) {//{{{
return new Promise((resolve, reject) => {
const t = this.db.transaction(['nodes', this.storeName], 'readwrite')
const store = t.objectStore(this.storeName)
@@ -366,7 +394,7 @@ class SimpleNodeStore {
}
})
}//}}}
- retrieve(limit) {//{{{
+ async retrieve(limit) {//{{{
return new Promise((resolve, reject) => {
const cursorReq = this.db
.transaction(['nodes', this.storeName], 'readonly')
@@ -394,7 +422,7 @@ class SimpleNodeStore {
}
})
}//}}}
- delete(keys) {//{{{
+ async delete(keys) {//{{{
const store = this.db
.transaction(['nodes', this.storeName], 'readwrite')
.objectStore(this.storeName)
@@ -411,7 +439,7 @@ class SimpleNodeStore {
}
return Promise.all(promises)
}//}}}
- count() {//{{{
+ async count() {//{{{
const store = this.db
.transaction(['nodes', this.storeName], 'readonly')
.objectStore(this.storeName)
diff --git a/static/js/sync.mjs b/static/js/sync.mjs
index fd606f3..edc93ea 100644
--- a/static/js/sync.mjs
+++ b/static/js/sync.mjs
@@ -1,6 +1,5 @@
import { API } from 'api'
import { Node } from 'node'
-import { CustomHTMLElement } from './lib/custom_html_element.mjs'
export class Sync {
constructor() {//{{{
@@ -155,43 +154,70 @@ export class Sync {
}//}}}
}
-export class N2SyncProgress extends CustomHTMLElement {
- static {
- this.tmpl = document.createElement('template')
- this.tmpl.innerHTML = `
-
-
0 / 0
- `
- }
- constructor() {//{{{
- super()
-
+export class SyncProgress {
+ constructor(parentEl) {//{{{
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 = `
+
+
0 / 0
+ `
+
+ 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.setSyncState(false)
+ this.state.syncedDone = true
// Don't update anything if nothing was synced.
if (this.state.nodesSynced === 0)
@@ -207,17 +233,17 @@ export class N2SyncProgress extends CustomHTMLElement {
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`
`
+ */
+
+
}//}}}
- setSyncState(state) {// {{{
- if (state)
- this.classList.add('show')
- else
- setTimeout(() => this.classList.remove('show'), 1500)
- }// }}}
}
-customElements.define('n2-syncprogress', N2SyncProgress)
// vim: foldmethod=marker
diff --git a/static/less/notes2.less b/static/less/notes2.less
new file mode 100644
index 0000000..2c57f53
--- /dev/null
+++ b/static/less/notes2.less
@@ -0,0 +1,365 @@
+@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: "";
+ }
+ }
+ }
+}
diff --git a/static/service_worker.js b/static/service_worker.js
index c48c162..6c77241 100644
--- a/static/service_worker.js
+++ b/static/service_worker.js
@@ -6,6 +6,13 @@ 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',
diff --git a/views/layouts/main.gotmpl b/views/layouts/main.gotmpl
index b3b5514..470cfe5 100644
--- a/views/layouts/main.gotmpl
+++ b/views/layouts/main.gotmpl
@@ -2,11 +2,21 @@
-
+
{{ end }}