From 9fc4a14ce3ff6063d629a288028ef027adcd5232 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Sun, 3 May 2026 09:17:20 +0200 Subject: [PATCH] Work on sync element, now a custom HTML element --- main.go | 2 +- static/css/notes2.css | 229 +++++++++++------------ static/js/app.mjs | 18 +- static/js/node.mjs | 380 ++------------------------------------ static/js/node_store.mjs | 62 ++----- static/js/sync.mjs | 68 +++---- static/less/notes2.less | 365 ------------------------------------ static/service_worker.js | 7 - views/layouts/main.gotmpl | 12 +- views/pages/notes2.gotmpl | 48 ++--- 10 files changed, 190 insertions(+), 1001 deletions(-) delete mode 100644 static/less/notes2.less diff --git a/main.go b/main.go index e608601..96f08aa 100644 --- a/main.go +++ b/main.go @@ -25,7 +25,7 @@ import ( const VERSION = "v1" const CONTEXT_USER = 1 -const SYNC_PAGINATION = 500 +const SYNC_PAGINATION = 200 var ( FlagGenerate bool diff --git a/static/css/notes2.css b/static/css/notes2.css index adfa51d..e6d0ef6 100644 --- a/static/css/notes2.css +++ b/static/css/notes2.css @@ -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,7 +45,14 @@ html { #tree { display: none; } + + n2-syncprogress { + .el-count { + top: 4px; + } + } } + } #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,143 +203,112 @@ html { } -#sync-progress { +n2-syncprogress { --radius: 8px; + display: grid; grid-area: sync; display: grid; justify-items: center; align-items: center; - width: 100%; - height: 56px; + position: relative; - .container { - position: relative; + opacity: 0; + transition: height 0s 500ms, opacity 500ms linear, visibility 0s 500ms; - 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); - } + &.show { + opacity: 1; + transition: visibility, height 0s, opacity 500ms linear; } - - - &.hidden { - visibility: hidden; - opacity: 0; - transition: visibility 0s 500ms, opacity 500ms linear; + 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; + } -#name { - color: #333; - font-weight: bold; - text-align: center; - font-size: 1.15em; - margin-top: 0px; - margin-bottom: 16px; -} + 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); + } -/* ============================================================= * - * 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; -} + 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); + } -.grow-wrap::after { - /* Note the weird space! Needed to preventy jumpy behavior */ - content: attr(data-replicated-value) " "; + 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); + } - /* 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; + 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 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; +n2-nodeui { + margin-bottom: 32px; - &:invalid { - background: #f5f5f5; - padding-top: 16px; + .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; + } } } diff --git a/static/js/app.mjs b/static/js/app.mjs index 89ecab3..c68475e 100644 --- a/static/js/app.mjs +++ b/static/js/app.mjs @@ -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': diff --git a/static/js/node.mjs b/static/js/node.mjs index 3820941..949724c 100644 --- a/static/js/node.mjs +++ b/static/js/node.mjs @@ -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`
_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} /> -
+export class N2NodeUI extends CustomHTMLElement { + static {// {{{ + this.tmpl = document.createElement('template') + this.tmpl.innerHTML = ` +
+ ` + }// }}} - - - - 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) {// {{{ + 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 = ` -
- - ` - 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) {//{{{ diff --git a/static/js/node_store.mjs b/static/js/node_store.mjs index ef43233..98642d1 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 }//}}} - 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) diff --git a/static/js/sync.mjs b/static/js/sync.mjs index edc93ea..fd606f3 100644 --- a/static/js/sync.mjs +++ b/static/js/sync.mjs @@ -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 = ` + +
0 / 0
+ ` + } + 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 = ` - -
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.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`
` - */ - - }//}}} + 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 deleted file mode 100644 index 2c57f53..0000000 --- a/static/less/notes2.less +++ /dev/null @@ -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: ""; - } - } - } -} diff --git a/static/service_worker.js b/static/service_worker.js index 6c77241..c48c162 100644 --- a/static/service_worker.js +++ b/static/service_worker.js @@ -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', diff --git a/views/layouts/main.gotmpl b/views/layouts/main.gotmpl index 470cfe5..b3b5514 100644 --- a/views/layouts/main.gotmpl +++ b/views/layouts/main.gotmpl @@ -2,21 +2,11 @@ - + {{ end }}