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} /> + + +
+
+
${crumbs} +
+
+ + ${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 }}