-
-
-
diff --git a/static/images/leaf_orphaned.svg b/static/images/leaf_orphaned.svg
deleted file mode 100644
index 8b1cc37..0000000
--- a/static/images/leaf_orphaned.svg
+++ /dev/null
@@ -1,61 +0,0 @@
-
-
-
-
diff --git a/static/js/api.mjs b/static/js/api.mjs
index 26a19de..3fff10a 100644
--- a/static/js/api.mjs
+++ b/static/js/api.mjs
@@ -1,7 +1,7 @@
export class API {
// query resolves into the JSON data produced by the application, or an exception with 'type' and 'error' properties.
static async query(method, path, request) {
- try {
+ return new Promise((resolve, reject) => {
const body = JSON.stringify(request)
const headers = {}
@@ -12,22 +12,33 @@ export class API {
headers.Authorization = `Bearer ${token}`
}
- const res = await fetch(path, { method, headers, body })
- // An HTTP communication level error occured.
- if (!res.ok || res.status != 200)
- throw new Error('HTTP error', { cause: { type: 'http', error: res, }})
-
- // Application level response are handled here.
- const json = await res.json()
- if (!json.OK)
- throw new Error(json.Error, { cause: { type: 'application', application: json, }})
-
- return json
-
- } catch (err) {
- // Catch any other errors from fetch.
- throw new Error(err.message, { cause: { type: 'http', error: err, }})
- }
+ fetch(path, { method, headers, body })
+ .then(response => {
+ // An HTTP communication level error occured.
+ if (!response.ok || response.status != 200)
+ return reject({
+ type: 'http',
+ error: response,
+ })
+ return response.json()
+ })
+ .then(json => {
+ // Application level response are handled here.
+ if (!json.OK)
+ return reject({
+ type: 'application',
+ error: json.Error,
+ application: json,
+ })
+ resolve(json)
+ })
+ .catch(err =>
+ // Catch any other errors from fetch.
+ reject({
+ type: 'http',
+ error: err,
+ }))
+ })
}
static hasAuthenticationToken() {//{{{
diff --git a/static/js/app.mjs b/static/js/app.mjs
index 90bad39..38aebd4 100644
--- a/static/js/app.mjs
+++ b/static/js/app.mjs
@@ -2,61 +2,51 @@ import { ROOT_NODE } from 'node_store'
import { CustomHTMLElement } from './lib/custom_html_element.mjs'
import { N2Sidebar } from 'sidebar'
import { Node } from 'node'
-import { N2PreferenceSet } from './page_preferences.mjs'
export class App {
- static PAGES = ['node', 'history', 'storage']
-
constructor() {// {{{
this.currentNode = null
this.sidebar = new N2Sidebar()
this.crumbs = new N2Crumbs()
this.crumbsElement = document.getElementById('crumbs')
this.nodeUI = document.getElementById('note')
- this.dragIcon = new N2DragIcon()
-
- this.preferences = this.getPreferences()
this.sidebar.render().then(sidebar => {
document.getElementById('tree').append(sidebar)
document.getElementById('tree-nodes')?.focus()
})
- // Start node shows a system-wide page instead of node editing
- // since the start node is kind of magic and doesn't fit into
- // the syncing system.
- const determineNodePage = uuid => {
- const el = document.getElementById('notes2')
- if (uuid == ROOT_NODE)
- el.classList.add('root-node-override')
- else
- el.classList.remove('root-node-override')
- }
-
_mbus.subscribe('TREE_RENDERED', async () => {
// Subscribing to the start node existing after the tree trunk is
// fetched since the NODE_COMPONENT_EXIST message isn't sent for the
// root node itself, and the root node should be selected in the tree
// after it is rendered when the site is shown without UUID in the URL.
const startNode = await this.getStartNode()
- determineNodePage(startNode.UUID)
- this.goToNode(startNode.UUID, false, false)
+
+ if (startNode.UUID == ROOT_NODE)
+ this.goToNode(startNode.UUID, false, false)
+ else
+ this.goToNode(startNode.UUID, false, false)
})
_mbus.subscribe('TREE_NODE_SELECTED', event => {
const node = event.detail.data
- determineNodePage(node.UUID)
this.goToNode(node.UUID, false, false)
})
_mbus.subscribe('GO_TO_NODE', event => {
const node = event.detail.data
- determineNodePage(node.nodeUUID)
this.goToNode(node.nodeUUID, node.dontPush, node.dontExpand)
})
_mbus.subscribe('SHOW_PAGE', ({ detail: { data: { page } } }) => {
- const classList = document.getElementById('notes2').classList
+ let classList = document.querySelector('#main-page').classList
+ classList.forEach(e =>
+ classList.remove(e)
+ )
+ classList.add(page)
+
+ classList = document.querySelector('#notes2').classList
classList.forEach(e => {
if (e.startsWith('page-'))
classList.remove(e)
@@ -64,11 +54,6 @@ export class App {
classList.add('page-' + page)
})
- _mbus.subscribe('DEVICE_PREFERENCE_SET_UPDATED', ()=>{
- this.preferences = this.getPreferences()
- console.log(this.preferences.data)
- })
-
window.addEventListener('keydown', event => this.keyHandler(event))
window.addEventListener('popstate', event => this.popState(event))
document.getElementById('notes2').addEventListener('click', event => {
@@ -76,9 +61,6 @@ export class App {
document.getElementById('node-content')?.focus()
})
- document.querySelector('#page-root .create').addEventListener('click', () => this.createNode())
- document.body.append(this.dragIcon)
-
_mbus.dispatch('SHOW_PAGE', { page: 'node' })
window._sync = new Sync()
@@ -88,52 +70,65 @@ export class App {
// There a slight delay to initiate sync seems reasonable.
setTimeout(() => window._sync.run(), 1000)
}// }}}
+
keyHandler(event) {//{{{
let handled = true
- // Most keybindings is Alt+Shift, since the popular browsers at the time (2023) allows to override thees.
+ // 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.
- const CTRL = !event.shiftKey && event.ctrlKey && !event.altKey
- const SHIFT_ALT = event.shiftKey && !event.ctrlKey && event.altKey
- const SHIFT_CTRL_ALT = event.shiftKey && event.ctrlKey && event.altKey
+ if (!(event.shiftKey && event.altKey) && !(event.key.toUpperCase() === 'S' && event.ctrlKey))
+ return
switch (event.key.toUpperCase()) {
- case 'F2':
- this.nodeUI.renameNode()
- break
case 'T':
- if (!SHIFT_ALT) { handled = false; break }
- if (document.activeElement.id === 'tree-nodes')
+ if (document.activeElement.id === 'tree-nodes') {
this.nodeUI.takeFocus()
- else
+ } else {
this.sidebar.focus()
+ }
break
case 'F':
- if (!SHIFT_ALT) { handled = false; break }
_mbus.dispatch('op-search')
break
+ /*
+ case 'C':
+ this.showPage('node')
+ break
+
+ case 'E':
+ this.showPage('keys')
+ break
+ */
case 'M':
- if (!SHIFT_ALT) { handled = false; break }
globalThis._mbus.dispatch('MARKDOWN_TOGGLE')
break
case 'N':
- if (SHIFT_ALT)
- this.createNode()
- else if (SHIFT_CTRL_ALT) {
- this.createNode(this.currentNode?.ParentUUID)
- } else {
- handled = false
- }
+ this.createNode()
break
+ /*
+ case 'P':
+ this.showPage('node-properties')
+ break
+
+ */
case 'S':
- if (!CTRL) { handled = false; break }
this.nodeUI.saveNode()
break
+ /*
+
+ case 'U':
+ this.showPage('upload')
+ break
+
+ case 'F':
+ this.showPage('search')
+ break
+ */
default:
handled = false
@@ -160,26 +155,17 @@ export class App {
async saveNode() {//{{{
}//}}}
- async moveNode(node, targetNodeUUID) {// {{{
- node.moveToParent(targetNodeUUID)
- await node.save()
- }// }}}
- async createNode(createUnderUUID) {//{{{
- const parentUUID = createUnderUUID ? createUnderUUID : this.currentNode.UUID
- const p = createUnderUUID ? 'Name for sibling document' : 'Name for sub-document'
-
- let name = prompt(p)
+ async createNode() {//{{{
+ let name = prompt("Name")
if (!name)
return
- const nn = Node.create(name, parentUUID)
- await nn.save()
+ const nn = Node.create(name, this.currentNode.UUID)
+ nn.save()
+
+ nodeStore.sendQueue.add(nn)
+ nodeStore.add([nn])
- // Treenode is forcefully rerendered and children refetched to both show the new node
- // and to get it resorted.
- const parentTreenode = this.sidebar.getTreeNode(parentUUID)
- await parentTreenode.render(true, true)
- _mbus.dispatch('GO_TO_NODE', { nodeUUID: nn.UUID })
}//}}}
async goToNode(nodeUUID, dontPush, dontExpand) {//{{{
if (nodeUUID === null || nodeUUID === undefined)
@@ -219,12 +205,6 @@ export class App {
let classList = document.querySelector('#main-page').classList
return classList.contains(page)
}// }}}
- getPreferences() {// {{{
- const devPrefSet = localStorage.getItem('device_preference_set') || 'default'
- const userData = localStorage.getItem('user') || '{"default": {}}'
- const user = JSON.parse(userData)
- return new N2PreferenceSet(devPrefSet, user.Preferences[devPrefSet])
- }// }}}
}
class N2Crumbs extends CustomHTMLElement {
@@ -258,6 +238,7 @@ class N2Crumbs extends CustomHTMLElement {
return this
}// }}}
}
+customElements.define('n2-crumbs', N2Crumbs)
class N2Crumb extends CustomHTMLElement {
static {// {{{
@@ -288,6 +269,7 @@ class N2Crumb extends CustomHTMLElement {
this.elLink.addEventListener('click', () => _mbus.dispatch("GO_TO_NODE", { nodeUUID: this.uuid, dontPush: false, dontExpand: true }))
}// }}}
}
+customElements.define('n2-crumb', N2Crumb)
function tmpl(html) {// {{{
const el = document.createElement('template')
@@ -361,52 +343,4 @@ class OpSearch extends Op {
}// }}}
}
-class N2DragIcon extends CustomHTMLElement {
- static {// {{{
- this.tmpl = document.createElement('template')
- this.tmpl.innerHTML = `
-
-
- `
- }// }}}
- constructor() {// {{{
- super(true)
-
- document.addEventListener('dragover', e => {
- this.style.left = `${e.clientX + 8}px`
- this.style.top = `${e.clientY}px`
- })
-
- this.dragSource = null
- }// }}}
- start() {// {{{
- this.style.display = 'block'
- }// }}}
- end() {// {{{
- this.style.display = 'none'
- }// }}}
- icon(name) {// {{{
- if (name != '')
- name = '_' + name
- this.elIcon.setAttribute('src', `/images/${_VERSION}/icon_drag${name}.svg`)
- }// }}}
- setSource(s) {// {{{
- this.dragSource = s
- }// }}}
- getSource() {// {{{
- return this.dragSource
- }// }}}
-}
-
-customElements.define('n2-crumbs', N2Crumbs)
-customElements.define('n2-crumb', N2Crumb)
-customElements.define('n2-dragicon', N2DragIcon)
-
// vim: foldmethod=marker
diff --git a/static/js/lib/custom_html_element.mjs b/static/js/lib/custom_html_element.mjs
index d1fb7ae..2cec808 100644
--- a/static/js/lib/custom_html_element.mjs
+++ b/static/js/lib/custom_html_element.mjs
@@ -1,17 +1,7 @@
-/* Use data-el or data-field attribute.
- * Element with data-el="hum-ding" is accessible as this.elHumDing and fields with
- * data-field="long-dong" as this.fieldLongDong.
- *
- * All field values can be retrieved with fieldValues() and uses the data-field attribute
- * as LongDong as key.
- */
-
export class CustomHTMLElement extends HTMLElement {
constructor(useShadow) {// {{{
super()
- this._fields = new Map()
-
const workOn = useShadow ? this.attachShadow({ mode: 'open' }) : this
workOn.appendChild(this.constructor.tmpl.content.cloneNode(true))
workOn.querySelectorAll('*').forEach(el => {
@@ -19,7 +9,6 @@ export class CustomHTMLElement extends HTMLElement {
if (field !== undefined) {
const fieldName = this.toElementName('field', field)
this[fieldName] = el
- this._fields.set(this.toElementName('', field), el)
}
const name = el.dataset.el
@@ -30,22 +19,39 @@ export class CustomHTMLElement extends HTMLElement {
}
})
}// }}}
- allFields() {// {{{
- return this._fields
- }// }}}
- fieldValues() {// {{{
- const state = {}
- for (const [name, field] of this._fields) {
- if (field.tagName.toLowerCase() == 'input' && field.getAttribute('type').toLowerCase() == 'checkbox')
- state[name] = field.checked
- else
- state[name] = field.value
-
- }
- return state
- }// }}}
toElementName(prefix, str) {// {{{
str = prefix + '-' + str
return str.replace(/-(id|[a-z])/g, match => match.toUpperCase().replace('-', ''))
}// }}}
}
+
+export class StupidPreactCustomHTMLElement extends HTMLElement {
+ constructor() {// {{{
+ super()
+
+ // Stupid stuff because of Preact.
+ this.clonedNodes = this.constructor.tmpl.content.cloneNode(true)
+ this.clonedNodes.querySelectorAll('*').forEach(el => {
+ const field = el.dataset.field
+ if (field !== undefined) {
+ const fieldName = this.toElementName('field', field)
+ this[fieldName] = el
+ }
+
+ const name = el.dataset.el
+ if (name !== undefined) {
+ const elName = this.toElementName('el', name)
+ this[elName] = el
+ el.classList.add('el-' + name)
+ }
+ })
+ }// }}}
+ toElementName(prefix, str) {// {{{
+ str = prefix + '-' + str
+ return str.replace(/-(id|[a-z])/g, match => match.toUpperCase().replace('-', ''))
+ }// }}}
+ connectedCallback() {// {{{
+ // Stupid stuff because of Preact.
+ this.appendChild(this.clonedNodes)
+ }// }}}
+}
diff --git a/static/js/marked_position.mjs b/static/js/marked_position.mjs
index 8c81eb4..175f490 100644
--- a/static/js/marked_position.mjs
+++ b/static/js/marked_position.mjs
@@ -92,9 +92,7 @@ function escapeHtmlEntities(html, encode) {// {{{
export class MarkedPosition {
constructor() {// {{{
- window.marked_setpos = (event) => this.setpos(event)
- window.marked_changecheckbox = (event) => this.changecheckbox(event)
- window.marked_copy_to_clipboard = (event, tagname) => this.copy_to_clipboard(event, tagname)
+ window.setpos = (event) => this.setpos(event)
this.render()
}// }}}
setpos(event) {// {{{
@@ -108,44 +106,8 @@ export class MarkedPosition {
}
})
}// }}}
- changecheckbox(event) {// {{{
- event.stopPropagation()
- event.preventDefault()
-
- _mbus.dispatch('MARKDOWN_CHANGE_CHECKBOX', {
- checkbox: event.target,
- position: {
- start: event.target.closest('[data-offset-start]').dataset.offsetStart,
- end: event.target.closest('[data-offset-start]').dataset.offsetEnd,
- }
- })
- }// }}}
- async copy_to_clipboard(event, tagname) {// {{{
- if (!event.shiftKey)
- return
-
- try {
- // Stop text selections on the page to the mouse pointer.
- // Old selections are remove as well to give a cleaner view
- // of the copied text/highlighting.
- event.preventDefault()
- event.stopPropagation()
- window.getSelection().removeAllRanges()
-
- const text = event.target.innerText
- await navigator.clipboard.writeText(text)
-
- const tagClasslist = event.target.closest(tagname).classList
- tagClasslist.add('copy')
- setTimeout(()=>tagClasslist.remove('copy'), 250)
-
- } catch (err) {
- console.error('Failed to copy: ', err)
- alert('Failed to copy: ', err)
- }
- }// }}}
-
render() {// {{{
+ const markedObject = this
this.marked = new Marked()
this.marked.use(markedTokenPosition())
this.marked.use({
@@ -153,8 +115,8 @@ export class MarkedPosition {
heading(token) {
const content = this.parser.parseInline(token.tokens)
return `
-
-
${content}\n
+
`
@@ -162,7 +124,7 @@ export class MarkedPosition {
paragraph(token) {
const content = this.parser.parseInline(token.tokens)
- return `
${content}
\n`
+ return `
${content}
\n`
},
list(token) {
@@ -181,7 +143,7 @@ export class MarkedPosition {
},
listitem(token) {
- return `
${this.parser.parse(token.tokens)}\n`
+ return `
${this.parser.parse(token.tokens)}\n`
},
code(token) {
@@ -190,12 +152,12 @@ export class MarkedPosition {
const code = token.text.replace(other.endingNewline, '') + '\n'
if (!langString) {
- return `
`
+ return ``
+ (token.escaped ? code : escapeHtmlEntities(code, true))
+ '
\n'
}
- return `'
+ (token.escaped ? code : escapeHtmlEntities(code, true))
@@ -204,7 +166,7 @@ export class MarkedPosition {
blockquote(token) {
const body = this.parser.parse(token.tokens)
- return `\n${body}
\n`
+ return `\n${body}
\n`
},
html(token) {
@@ -216,13 +178,13 @@ export class MarkedPosition {
},
hr(token) {
- return `
\n`
+ return `
\n`
},
checkbox(token) {
- return ` '
+ + 'disabled="" type="checkbox"> '
},
table(token) {
@@ -265,7 +227,7 @@ export class MarkedPosition {
if (token.tokens.length > 0) {
const start = token.tokens[0].position.start.offset
const end = token.tokens[0].position.end.offset
- ofs = `ondblclick="marked_setpos(event)" data-offset-start="${start}" data-offset-end="${end}"`
+ ofs = `ondblclick="setpos(event)" data-offset-start="${start}" data-offset-end="${end}"`
}
const content = this.parser.parseInline(token.tokens);
@@ -277,23 +239,23 @@ export class MarkedPosition {
},
strong(token) {
- return `${this.parser.parseInline(token.tokens)}`
+ return `${this.parser.parseInline(token.tokens)}`
},
em(token) {
- return `${this.parser.parseInline(token.tokens)}`
+ return `${this.parser.parseInline(token.tokens)}`
},
codespan(token) {
- return `${escapeHtmlEntities(token.text, true)}`
+ return `${escapeHtmlEntities(token.text, true)}`
},
br(token) {
- return `
`
+ return `
`
},
del(token) {
- return `${this.parser.parseInline(token.tokens)}`
+ return `${this.parser.parseInline(token.tokens)}`
},
link(token) {
@@ -303,7 +265,7 @@ export class MarkedPosition {
return text
}
token.href = cleanHref
- let out = ' {
@@ -61,7 +57,7 @@ export class NodeStore {
break
case 6:
- nodesHistory = db.createObjectStore('nodes_history', { keyPath: ['UUID', 'HistoryUUID'] })
+ nodesHistory = db.createObjectStore('nodes_history', { keyPath: ['UUID', 'Updated'] })
break
case 7:
@@ -80,7 +76,8 @@ export class NodeStore {
this.sendQueue = new SimpleNodeStore(this.db, 'send_queue')
this.nodesHistory = new NodeHistoryStore(this.db, 'nodes_history')
this.files = new SimpleNodeStore(this.db, 'files')
- resolve()
+ this.initializeRootNode()
+ .then(() => resolve())
}
req.onerror = (event) => {
@@ -88,11 +85,40 @@ export class NodeStore {
}
})
}//}}}
- initializeSpecialNodes() {// {{{
- this.nodes[ROOT_NODE] = new Node({ UUID: ROOT_NODE, Name: 'Start', Special: true }, -1)
- this.nodes[DELETED_NODE] = new Node({ UUID: DELETED_NODE, Name: 'Deleted nodes', Special: true }, -1)
- this.nodes[ORPHANED_NODE] = new Node({ UUID: ORPHANED_NODE, Name: 'Orphaned nodes', Special: true }, -1)
- }// }}}
+ 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.
+ const trx = this.db.transaction('nodes', 'readwrite')
+ const nodes = trx.objectStore('nodes')
+ const getRequest = nodes.get(ROOT_NODE)
+ getRequest.onsuccess = (event) => {
+ // Root node exists - nice!
+ if (event.target.result !== undefined) {
+ resolve(event.target.result)
+ return
+ }
+
+ const putRequest = nodes.put({
+ UUID: ROOT_NODE,
+ Name: 'Notes2',
+ Content: 'Hello, World!',
+ Updated: new Date().toISOString(),
+ ParentUUID: '',
+ })
+ putRequest.onsuccess = (event) => {
+ resolve(event.target.result)
+ }
+ putRequest.onerror = (event) => {
+ reject(event.target.error)
+ }
+ }
+ getRequest.onerror = (event) => reject(event.target.error)
+ })
+ }//}}}
+ purgeCache() {//{{{
+ this.nodes = {}
+ }//}}}
node(uuid, dataIfUndefined, newLevel) {//{{{
let n = this.nodes[uuid]
@@ -221,7 +247,6 @@ export class NodeStore {
nodeStore = t.objectStore('nodes')
t.oncomplete = (_event) => {
- console.log('complete')
resolve()
}
@@ -246,14 +271,6 @@ export class NodeStore {
}//}}}
get(uuid, suppliedNodestore) {//{{{
return new Promise((resolve, reject) => {
- switch (uuid) {
- case ROOT_NODE:
- case DELETED_NODE:
- case ORPHANED_NODE:
- resolve(this.nodes[uuid])
- return
- }
-
// A nodestore can be provided in order to
// avoid creating new transactions.
let trx
@@ -291,16 +308,6 @@ export class NodeStore {
return
}
- if (node.UUID === DELETED_NODE || node.ParentUUID === DELETED_NODE) {
- resolve(accumulated)
- return
- }
-
- if (node.UUID === ORPHANED_NODE || node.ParentUUID === ORPHANED_NODE) {
- resolve(accumulated)
- return
- }
-
const getRequest = nodeParentIndex.get(node.ParentUUID)
getRequest.onsuccess = (event) => {
// Node not found in IndexedDB.
@@ -351,7 +358,6 @@ class SimpleNodeStore {
// Node to be moved is first stored in the new queue.
const req = store.put(node.data)
req.onsuccess = () => {
- console.log('here')
resolve()
}
req.onerror = (event) => {
@@ -467,15 +473,11 @@ class NodeHistoryStore extends SimpleNodeStore {
}// }}}
retrievePage(uuid, perPage, page) {// {{{
return new Promise((resolve, _reject) => {
-
- const lowerBound = [uuid, '00000000-0000-0000-0000-000000000000']
- const upperBound = [uuid, 'ffffffff-ffff-ffff-ffff-ffffffffffff']
- const range = IDBKeyRange.bound(lowerBound, upperBound)
-
const cursor = this.db
.transaction(['nodes', this.storeName], 'readonly')
.objectStore(this.storeName)
- .openCursor(range, 'prev')
+ .index('byUUID')
+ .openCursor(uuid, 'prev')
let retrieved = 0
let first = true
diff --git a/static/js/page_node.mjs b/static/js/page_node.mjs
index 2106ada..cca6cf0 100644
--- a/static/js/page_node.mjs
+++ b/static/js/page_node.mjs
@@ -2,86 +2,21 @@ import { ROOT_NODE, uuidv7 } from 'node_store'
import { CustomHTMLElement } from './lib/custom_html_element.mjs'
import { MarkedPosition } from './marked_position.mjs'
-class N2NodeMenu extends CustomHTMLElement {
- static {// {{{
- this.tmpl = document.createElement('template')
- this.tmpl.innerHTML = `
-
-
- `
- }// }}}
- constructor() {// {{{
- super()
- }// }}}
-}
-customElements.define('n2-nodemenu', N2NodeMenu)
-
export class N2PageNodeUI extends CustomHTMLElement {
static {// {{{
this.tmpl = document.createElement('template')
this.tmpl.innerHTML = `
@@ -92,13 +27,9 @@ export class N2PageNodeUI extends CustomHTMLElement {
-
`
}// }}}
@@ -107,14 +38,12 @@ export class N2PageNodeUI extends CustomHTMLElement {
this.node = null
this.style.display = 'contents'
+ this.classList.add('show-markdown') // TODO Should probably be moved to settings.
this.marked = new MarkedPosition()
_mbus.subscribe('NODE_UI_OPEN', event => {
this.node = event.detail.data
-
-
- if (!this.node.isSpecial())
- this.showMarkdown(true)
+ this.showMarkdown(true)
this.render()
})
@@ -133,29 +62,23 @@ export class N2PageNodeUI extends CustomHTMLElement {
_mbus.subscribe('MARKDOWN_TOGGLE', () => this.showMarkdown(!this.showMarkdown()))
_mbus.subscribe('MARKDOWN_EDIT', ({ detail }) => this.editMarkdown(detail.data))
- _mbus.subscribe('MARKDOWN_CHANGE_CHECKBOX', ({ detail }) => this.checkboxUpdated(detail.data))
- // Binding the node rename handler.
- this.elName.addEventListener('click', async () => this.renameNode())
+ this.elName.addEventListener('click', () => {
+ const name = prompt('Change title', this.node.data.Name)
+ if (name === null)
+ return
- // Bind handlers for content keyboard input and paste.
+ try {
+ this.node.setName(name)
+ } catch (err) {
+ console.error(err)
+ alert(err)
+ }
+ })
this.elNodeContent.addEventListener('input', event => this.contentChanged(event))
this.elNodeContent.addEventListener('paste', async (event) => this.pasteHandler(event))
-
- // Bind node icon handlers.
- this.elIconSave.addEventListener('click', () => this.saveNode())
this.elIconMarkdown.addEventListener('click', () => this.showMarkdown(!this.showMarkdown()))
- this.elIconNewDocument.addEventListener('click', event => {
- if (event.shiftKey)
- _app.createNode(this.node.ParentUUID)
- else
- _app.createNode()
- })
-
- // Bind node menu items to handlers.
- this.elNodeMenu.elFormatTables.addEventListener('click', event => {
- this.elNodeMenu.hidePopover()
-
+ this.elIconTableFormat.addEventListener('click', event => {
if (!event.shiftKey)
this.elNodeContent.value = this.formatAllTables(this.elNodeContent.value)
else {
@@ -169,12 +92,9 @@ export class N2PageNodeUI extends CustomHTMLElement {
this.node.setContent(this.elNodeContent.value)
})
- this.elNodeMenu.elHistory.addEventListener('click', () => {
- _mbus.dispatch('SHOW_PAGE', { page: 'history' })
- })
+ this.elIconHistory.addEventListener('click', () => _mbus.dispatch('SHOW_PAGE', { page: 'history' }))
+ this.elIconSave.addEventListener('click', ()=>this.saveNode())
- // Default is to always show markdown.
- this.classList.add('show-markdown') // TODO Should probably be moved to settings.
this.showMarkdown(true)
}// }}}
renderName() {// {{{
@@ -191,37 +111,35 @@ export class N2PageNodeUI extends CustomHTMLElement {
} else
this.elNodeContent.focus({ preventScroll: true })
}// }}}
- async renameNode() {// {{{
- const name = prompt('Change title', this.node.data.Name)
- if (name === null)
- return
-
- try {
- // Document isn't only renamed, but also saved at once.
- // Not really correct, but good enough to not have to implement
- // a separate way to only rename the document. Since history is
- // preserved it shouldn't be that horrible.
- this.node.setName(name)
- await this.node.save()
-
- // Re-render the parent treenode forcefully to sort it again.
- const parentUUID = this.node.ParentUUID
- if (!parentUUID)
- return
- const parentTreeNode = _app.sidebar.getTreeNode(parentUUID)
- parentTreeNode?.render(true, true)
- } catch (err) {
- console.error(err)
- alert(err)
- }
- }// }}}
async saveNode() {// {{{
if (!this.node.isModified())
return
- // node.save takes care of both "nodes" and "nodes_history" stores, also adds it to send queue.
- // Sets "Updated" value to current date and time and generates a new history UUID.
+ /* 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. */
+
+ // The node is still in its old state and will present
+ // the unmodified content to the node store.
+ const history = nodeStore.nodesHistory.add(this.node)
+
+ // Prepares the node object for saving.
+ // Sets Updated value to current date and time.
await this.node.save()
+
+ // Updated node is added to the send queue to be stored on server.
+ const sendQueue = nodeStore.sendQueue.add(this.node)
+
+ // Updated node is saved to the primary node store.
+ const nodeStoreAdding = nodeStore.add([this.node])
+
+ await Promise.all([history, sendQueue, nodeStoreAdding])
}// }}}
contentChanged(event) {//{{{
@@ -371,30 +289,6 @@ export class N2PageNodeUI extends CustomHTMLElement {
return lines
}// }}}
- // "marked" sends a messagebus event when checking/unchecking a checkbox.
- // Updates node and content textarea.
- checkboxUpdated(eventData) {// {{{
- const checkbox = eventData.checkbox
- const pos = eventData.position
- const content = this.node.content()
-
- // Basic validation to verify that Marked does what is known and expected at this writing.
- const mdCheckboxStr = content.slice(pos.start, pos.end)
- if (!mdCheckboxStr.match(/^\[[ xX]\] $/)) {
- alert(`Checkbox string didn't pass validation: '${mdCheckboxStr}'`)
- console.error(`Checkbox string didn't pass validation: '${mdCheckboxStr}'`)
- }
-
- // Node is modified with the new value. User has to save manually, otherwise other changes could be saved
- // when a save wasn't expected.
- const newValue = `[${checkbox.checked ? 'x' : ' '}] `
- const modifiedContent = this.node.content().slice(0, pos.start) + newValue + this.node.content().slice(pos.end)
- this.node.setContent(modifiedContent)
-
- // Also update the textarea since the node model doesn't know about it.
- this.elNodeContent.setRangeText(newValue, pos.start, pos.end, 'select')
-
- }// }}}
}
customElements.define('n2-nodeui', N2PageNodeUI)
@@ -412,20 +306,15 @@ export class Node {
return 0
}//}}}
static create(name, parentUUID) {// {{{
- const node = new Node({
+ return new Node({
UUID: uuidv7(),
Created: (new Date()).toISOString(),
Content: '',
Name: name,
ParentUUID: parentUUID,
Markdown: false,
+ History: false,
})
-
- // Newly created node (not constructed from existing data) is considered modified
- // since node.save returns early if it isn't modified.
- node._modified = true
-
- return node
}// }}}
constructor(nodeData, level) {//{{{
@@ -504,23 +393,12 @@ export class Node {
getParent() {//{{{
return this._parent
}//}}}
- moveToParent(newParentUUID) {// {{{
- if (this.UUID === newParentUUID)
- throw new Error("New parent UUID is the same as node UUID. Can't be your own parent.")
-
- this.ParentUUID = newParentUUID
- this.data.ParentUUID = newParentUUID
- this._modified = true
- }// }}}
isLastSibling() {//{{{
return this._sibling_after === null
}//}}}
isFirstSibling() {//{{{
return this._sibling_before === null
}//}}}
- isSpecial() {// {{{
- return this.data.Special
- }// }}}
content() {//{{{
/* TODO - implement crypto
if (this.CryptoKeyID != 0 && !this._decrypted)
@@ -542,52 +420,17 @@ export class Node {
_mbus.dispatch('NODE_MODIFIED', { node: this })
}// }}}
async save() {//{{{
- // Just safeguarding not using the root node,
- // which sort of exist but isn't supposed to communicate to server.
- if (this.UUID == ROOT_NODE)
- return
-
this.data.Content = this._content
this.data.Updated = new Date().toISOString()
- this.data.HistoryUUID = uuidv7() // every time the node is saved a new history UUID identifies the changed node.
this._modified = false
_mbus.dispatch('NODE_UNMODIFIED')
// When stored into database and ancestry was changed,
// the ancestry path could be interesting.
- /*
const ancestors = await nodeStore.getNodeAncestry(this)
this.data.Ancestors = ancestors.map(a => a.get('Name')).reverse()
- */
- /* 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. */
-
- // Current node is added to history. It will be duplicated with the "nodes" store
- // for simplicity, to hopefully avoid bugs.
- const history = nodeStore.nodesHistory.add(this)
-
- // Updated node is added to the send queue to be stored on server.
-
- const sendQueue = nodeStore.sendQueue.add(this)
-
- // Updated node is saved to the primary node store.
- const nodeStoreAdding = nodeStore.add([this])
-
- console.log('waiting')
- await Promise.all([history, sendQueue, nodeStoreAdding])
- console.log('waiting done')
-
- return
}//}}}
}
-
// vim: foldmethod=marker
diff --git a/static/js/page_preferences.mjs b/static/js/page_preferences.mjs
deleted file mode 100644
index 9655278..0000000
--- a/static/js/page_preferences.mjs
+++ /dev/null
@@ -1,283 +0,0 @@
-import { CustomHTMLElement } from "./lib/custom_html_element.mjs"
-import { API } from './api.mjs'
-
-export class N2PagePreferences extends CustomHTMLElement {
- static {// {{{
- this.tmpl = document.createElement('template')
- this.tmpl.innerHTML = `
-
- Preferences
-
- Changes preferences to not download images or files on the device doesn't remove the already downloaded data.
-
-
-
Device preference set
-
-
-
-
-
-
-
- `
- }// }}}
- constructor() {// {{{
- super(true)
- this.sets = []
-
- this.elNewSet.addEventListener('click', () => this.newSet())
- this.elSave.addEventListener('click', () => this.save())
- this.elDevPreferenceSet.addEventListener('change', event=>this.changePreferenceSet(event))
-
- window._mbus.subscribe('SHOW_PAGE', async event => {
- if (event.detail.data?.page == 'preferences') {
- this.sets = await this.getPreferenceSets()
- this.render()
- }
- })
-
- window._mbus.subscribe('PREFERENCE_SET_MODIFIED', () => this.preferencesModified())
- window._mbus.subscribe('PREFERENCE_SET_DELETE', event => this.preferencesDelete(event.detail.data.set))
- }// }}}
- sortSets(a, b) {// {{{
- if (a.name == 'default') return -1
- if (b.name == 'default') return 1
-
- if (a.name.toLowerCase() < b.name.toLowerCase()) return -1
- if (a.name.toLowerCase() > b.name.toLowerCase()) return 1
-
- return 0
- }// }}}
- async render() {// {{{
- try {
- this.sets.sort(this.sortSets)
- this.elSets.replaceChildren(...this.sets)
-
- const setNames = this.sets.entries().map(([i, set]) => {
- const optn = document.createElement('option')
- optn.innerText = set.name
- return optn
- })
- this.elDevPreferenceSet.replaceChildren(...setNames)
- } catch (e) {
- console.error(e)
- alert(e.message)
- }
- }// }}}
- async getPreferenceSets() {// {{{
- const userData = localStorage.getItem('user')
- if (userData === null)
- throw new Error('Could not find user in localStorage')
-
- const user = JSON.parse(userData)
- const prefsData = user.Preferences
-
- if (prefsData === undefined)
- throw new Error('User object is missing preferences')
-
- if (!prefsData.hasOwnProperty('default'))
- throw new Error('The "default" preferences set is missing')
-
- return Object.keys(prefsData).map(name => new N2PreferenceSet(name, prefsData[name]))
- }// }}}
- async retrieveServerPreferences() {// {{{
- try {
- API.query('GET', '/user/preferences')
- } catch (e) {
- console.error(e)
- alert(`Error retrieving preferences: ${e.message}`)
- }
- }// }}}
- changePreferenceSet(event) {// {{{
- this.preferencesModified()
- }// }}}
- newSet() {// {{{
- let name = prompt("Name for new preference set")
- if (!name)
- return
-
- name = name.trim()
- if (name === '')
- return
-
- if (name == 'default') {
- alert(`Name can't be "default".`)
- return
- }
-
- const exists = this.sets.some(s => s.name.toLowerCase() == name.toLowerCase())
- if (exists) {
- alert(`Set with name "${name}" already exist.`)
- return
- }
-
- this.sets.push(new N2PreferenceSet(name, {}))
- this.preferencesModified()
- this.render()
- }// }}}
- preferencesModified() {// {{{
- this.elSave.removeAttribute('disabled')
- }// }}}
- preferencesDelete(deleteSet) {// {{{
- if (deleteSet.name == 'default') {
- alert("Can't delete the default set.")
- return
- }
-
- if (!confirm(`Confirm deleting "${deleteSet.name}"`))
- return
-
- this.sets = this.sets.filter(set => {
- return !(set.name === deleteSet.name)
- })
-
- this.preferencesModified()
- this.render()
- }// }}}
- async save() {// {{{
- try {
- let newPrefs = {}
- this.sets.forEach(s => {
- const setState = s.getState()
- newPrefs[setState.name] = setState.state
- })
-
- // Throws exception on both HTTP and application errors.
- await API.query('POST', '/user/preferences', newPrefs)
-
- const userData = localStorage.getItem('user')
- const user = JSON.parse(userData)
- user.Preferences = newPrefs
- localStorage.setItem('user', JSON.stringify(user))
- localStorage.setItem('device_preference_set', this.elDevPreferenceSet.value)
- _mbus.dispatch('DEVICE_PREFERENCE_SET_UPDATED')
- } catch (e) {
- console.error(e)
- alert(e.message)
- } finally {
- this.elSave.setAttribute('disabled', true)
- }
-
- }// }}}
-}
-customElements.define('n2-pagepreferences', N2PagePreferences)
-
-// Preferences is a set of preferences, of which there can be many named.
-export class N2PreferenceSet extends CustomHTMLElement {
- static {// {{{
- this.tmpl = document.createElement('template')
- this.tmpl.innerHTML = `
-
-
-
-
-
-
-
-
-
- `
- }// }}}
- constructor(name, data) {// {{{
- super(true)
- this.name = name
- this.data = data
- this.render()
-
- // Enable the save button when settings are modified.
- this.allFields().forEach(f =>
- f.addEventListener('input', () => _mbus.dispatch('PREFERENCE_SET_MODIFIED'))
- )
-
- this.elName.addEventListener('click', () => this.updateName())
- this.elDelete.addEventListener('click', () => this.deleteSet())
- }// }}}
- updateName() {// {{{
- if (this.name == 'default') {
- alert('Can not change name of the default profile.')
- return
- }
-
- const name = prompt("Change name", this.name)
- if (!name)
- return
-
- this.name = name
- this.render()
- _mbus.dispatch('PREFERENCE_SET_MODIFIED')
- }// }}}
- deleteSet() {// {{{
- _mbus.dispatch('PREFERENCE_SET_DELETE', { set: this })
- }// }}}
- render() {// {{{
- this.elName.innerText = this.name
-
- this.fieldDownloadImages.checked = this.data.DownloadImages
- this.fieldDownloadFiles.checked = this.data.DownloadFiles
- }// }}}
- getState() {// {{{
- const name = this.name.trim()
- if (name === '')
- throw new Error('Name can not be empty.')
-
- return {
- name: this.name.trim(),
- state: this.fieldValues(),
- }
- }// }}}
-}
-customElements.define('n2-preferenceset', N2PreferenceSet)
diff --git a/static/js/page_storage.mjs b/static/js/page_storage.mjs
index a007130..931a718 100644
--- a/static/js/page_storage.mjs
+++ b/static/js/page_storage.mjs
@@ -13,10 +13,7 @@ export class N2PageStorage extends CustomHTMLElement {
constructor() {
super()
- window._mbus.subscribe('SHOW_PAGE', event => {
- if (event.detail.data?.page == 'storage')
- this.render()
- })
+ window._mbus.subscribe('SHOW_PAGE', () => this.render())
}
async render() {
const countNodes = await globalThis.nodeStore.nodeCount()
diff --git a/static/js/sidebar.mjs b/static/js/sidebar.mjs
index 6cd5814..da05750 100644
--- a/static/js/sidebar.mjs
+++ b/static/js/sidebar.mjs
@@ -1,5 +1,4 @@
-import { ROOT_NODE, ORPHANED_NODE, DELETED_NODE } from 'node_store'
-import { Node } from 'node'
+import { ROOT_NODE } from 'node_store'
import { CustomHTMLElement } from './lib/custom_html_element.mjs'
import { Color, Solver } from './lib/css_colorize.mjs'
@@ -118,6 +117,7 @@ export class N2Sidebar extends CustomHTMLElement {
this.tabIndex = 0
this.treeNodeComponents = {}
+ this.treeTrunk = []
this.expandedNodes = {} // keyed on UUID
this.selectedNode = null
this.rendered = false
@@ -128,7 +128,6 @@ export class N2Sidebar extends CustomHTMLElement {
this.elSearch.addEventListener('click', () => _mbus.dispatch('op-search'))
this.elSync.addEventListener('click', () => _sync.run())
this.elLogo.addEventListener('click', () => _app.goToNode(ROOT_NODE, false, false))
- this.elSettings.addEventListener('click', ()=> _mbus.dispatch('SHOW_PAGE', { page: 'preferences' }))
this.elHideTree.addEventListener('click', event => {
event.stopPropagation()
_mbus.dispatch('TREE_EXPANSION', { expand: false })
@@ -158,26 +157,8 @@ export class N2Sidebar extends CustomHTMLElement {
this.expandedNodes[ROOT_NODE] = true
const startnode = await nodeStore.get(ROOT_NODE)
const starttreenode = new N2TreeNode(this, startnode, null)
-
- const deletednode = await nodeStore.get(DELETED_NODE)
- const deletedtreenode = new SpecialNodeDeleted(this, deletednode, null)
-
- const orphanednode = await nodeStore.get(ORPHANED_NODE)
- const orphanedtreenode = new SpecialNodeOrphaned(this, orphanednode, null)
-
- startnode._sibling_after = deletednode
- deletednode._sibling_before = startnode
-
- deletednode._sibling_after = orphanednode
- orphanednode._sibling_before = deletednode
-
this.treeNodeComponents[startnode.UUID] = starttreenode
- this.treeNodeComponents[deletednode.UUID] = deletedtreenode
- this.treeNodeComponents[orphanednode.UUID] = orphanedtreenode
-
this.elTreenodes.appendChild(await starttreenode.render())
- this.elTreenodes.appendChild(await deletedtreenode.render())
- this.elTreenodes.appendChild(await orphanedtreenode.render())
// Notify the application that the initial tree is rendered (with children)
// and that initial node selection can take place. App will check URL to
@@ -189,17 +170,19 @@ export class N2Sidebar extends CustomHTMLElement {
}// }}}
reset() {// {{{
this.treeNodeComponents = {}
+ this.treeTrunk = []
this.rendered = false
this.elTreenodes.replaceChildren()
- this.render()
+ this.populateFirstLevel()
}// }}}
getNodeExpanded(UUID) {//{{{
if (this.expandedNodes[UUID] === undefined)
this.expandedNodes[UUID] = false
return this.expandedNodes[UUID]
}//}}}
- async setNodeExpanded(node, value) {//{{{
+ setNodeExpanded(node, value) {//{{{
let expanded = this.expandedNodes[node.UUID]
+
if (expanded === undefined) {
this.expandedNodes[node.UUID] = false
expanded = false
@@ -231,9 +214,6 @@ export class N2Sidebar extends CustomHTMLElement {
isSelected(node) {//{{{
return this.selectedNode?.UUID === node.UUID
}//}}}
- getTreeNode(uuid) {// {{{
- return this.treeNodeComponents[uuid]
- }// }}}
async keyHandler(event) {//{{{
let handled = true
@@ -249,6 +229,8 @@ export class N2Sidebar extends CustomHTMLElement {
// Holding shift down does it recursively.
case Space:
case 'Enter':
+ if (n.UUID === ROOT_NODE)
+ return
const expanded = this.getNodeExpanded(n.UUID)
if (event.shiftKey) {
this.recursiveExpand(n, !expanded)
@@ -257,31 +239,38 @@ export class N2Sidebar extends CustomHTMLElement {
}
break
+ case 'g':
case 'Home':
this.navigateTop()
break
+ case 'G':
case 'End':
this.navigateBottom()
break
+ case 'j':
case 'ArrowDown':
await this.navigateDown(this.selectedNode)
break
+ case 'k':
case 'ArrowUp':
await this.navigateUp(this.selectedNode)
break
+ case 'h':
case 'ArrowLeft':
await this.navigateLeft(this.selectedNode)
break
+ case 'l':
case 'ArrowRight':
await this.navigateRight(this.selectedNode)
break
default:
+ // nonsole.log(event.key)
handled = false
}
@@ -291,7 +280,7 @@ export class N2Sidebar extends CustomHTMLElement {
}
}//}}}
async navigateLeft(n) {//{{{
- if (n === null || n === undefined || n.UUID == ROOT_NODE)
+ if (n === null || n === undefined)
return
const expanded = this.getNodeExpanded(n.UUID)
@@ -341,7 +330,7 @@ export class N2Sidebar extends CustomHTMLElement {
_mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getSiblingAfter()?.UUID, dontPush: false, dontExpand: true })
}//}}}
async navigateUp(n) {//{{{
- if (n === null || n === undefined || n.UUID == ROOT_NODE)
+ if (n === null || n === undefined)
return
let parent = null
@@ -403,26 +392,23 @@ export class N2Sidebar extends CustomHTMLElement {
}//}}}
async navigateTop() {//{{{
const root = await nodeStore.get(ROOT_NODE)
- _mbus.dispatch("GO_TO_NODE", { nodeUUID: root.UUID, dontPush: false, dontExpand: true })
+ if (root.Children.length === 0)
+ return
+ _mbus.dispatch("GO_TO_NODE", { nodeUUID: root.Children[0]?.UUID, dontPush: false, dontExpand: true })
}//}}}
async navigateBottom() {//{{{
- const orphaned = await nodeStore.get(ORPHANED_NODE)
-
- if (!orphaned.hasChildren() || this.getNodeExpanded(orphaned.UUID)) {
- _mbus.dispatch("GO_TO_NODE", { nodeUUID: orphaned.UUID, dontPush: false, dontExpand: true })
+ const root = await nodeStore.get(ROOT_NODE)
+ if (root.Children.length === 0)
return
- }
- /* TODO - fix this when orphaned nodes are implemented.
- const toplevel = orphaned.Children[orphaned.Children.length - 1]
+ const toplevel = root.Children[root.Children.length - 1]
const toplevelExpanded = this.getNodeExpanded(toplevel?.UUID)
if (toplevelExpanded) {
const lastnode = this.getLastExpandedNode(toplevel)
_mbus.dispatch("GO_TO_NODE", { nodeUUID: lastnode?.UUID, dontPush: false, dontExpand: true })
} else
- _mbus.dispatch("GO_TO_NODE", { nodeUUID: orphaned.Children[orphaned.Children.length - 1]?.UUID, dontPush: false, dontExpand: true })
- */
+ _mbus.dispatch("GO_TO_NODE", { nodeUUID: root.Children[root.Children.length - 1]?.UUID, dontPush: false, dontExpand: true })
}//}}}
getParentWithNextSibling(node) {//{{{
@@ -443,10 +429,6 @@ export class N2Sidebar extends CustomHTMLElement {
if (state)
await this.setNodeExpanded(node, true)
- // An expanded node needs to have its children fetched.
- if (!node.hasFetchedChildren())
- await node.fetchChildren()
-
for (const child of node.Children)
await this.recursiveExpand(child, state)
@@ -466,22 +448,15 @@ export class N2Sidebar extends CustomHTMLElement {
treenode?.scrollIntoView({ block: 'nearest' })
}// }}}
}
+customElements.define('n2-sidebar', N2Sidebar)
export class N2TreeNode extends CustomHTMLElement {
- static DRAG_ICON = new Image()
- static DRAG_ICON_OK = new Image()
-
static {// {{{
- N2TreeNode.DRAG_ICON.src = `/images/${_VERSION}/leaf.svg`
- N2TreeNode.DRAG_ICON_OK.src = `/images/${_VERSION}/expanded.svg`
-
this.tmpl = document.createElement('template')
this.tmpl.innerHTML = `
-
![]()
+
@@ -564,7 +489,6 @@ export class N2TreeNode extends CustomHTMLElement {
constructor(sidebar, node, parent) {//{{{
super()
- this.setAttribute('draggable', 'true')
this.classList.add('node')
this.sidebar = sidebar
@@ -573,100 +497,13 @@ export class N2TreeNode extends CustomHTMLElement {
this.children_populated = false
this.rendered = false
- this.dragNode = null
- this.elExpandToggle.addEventListener('click', event => {
- if (this.node.hasChildren())
- this.expandNode(event)
- else
- _mbus.dispatch('TREE_NODE_SELECTED', this.node)
- })
+ this.elExpandToggle.addEventListener('click', () => this.sidebar.setNodeExpanded(this.node, !this.sidebar.getNodeExpanded(this.node.UUID)))
this.elName.addEventListener('click', () => _mbus.dispatch('TREE_NODE_SELECTED', this.node))
_mbus.subscribe(`NODE_EXPAND_${node.UUID}`, _state => {
this.render(true)
})
-
- // Drag-and-dropping of nodes
- this.addEventListener('dragstart', event => this.dragStart(event))
- this.addEventListener('dragend', event => this.dragEnd(event))
- this.addEventListener('dragover', event => this.dragOver(event))
- this.addEventListener('drop', event => this.dragDrop(event))
- this.elName.addEventListener('dragenter', event => this.dragEnter(event))
- this.elName.addEventListener('dragleave', event => this.dragLeave(event))
- }// }}}
-
- dragStart(e) {// {{{
- if (this.node.isModified()) {
- alert('Save note before moving it.')
- e.stopPropagation()
- e.preventDefault()
- return
- }
-
- this.classList.add('drag-source')
- const blankPixel = new Image()
- blankPixel.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
- e.dataTransfer.setDragImage(blankPixel, 0, 0)
- e.dataTransfer.allowedEffects = 'none'
- e.stopPropagation()
- _app.dragIcon.setSource(this)
- _app.dragIcon.start()
- }// }}}
- dragEnd(e) {// {{{
- this.classList.remove('drag-source')
- _app.dragIcon.end()
- e.stopPropagation()
- }// }}}
- dragOver(e) {// {{{
- e.dataTransfer.dropEffect = 'move'
- e.preventDefault()
- }// }}}
- async dragDrop(e) {// {{{
- try {
- e.stopPropagation()
- const sourceNode = _app.dragIcon.getSource()
-
- // Abort if user drops the node back on itself.
- if (sourceNode.node.UUID === this.node.UUID)
- return
-
- await _app.moveNode(sourceNode.node, this.node.UUID)
-
- _app.sidebar.setNodeExpanded(this, true)
- await this.render(true, true)
- await sourceNode.render(true, true)
- } catch (e) {
- console.error(e)
- alert(e)
- } finally {
- this.dragLeave(e)
- }
- }// }}}
- dragEnter(e) {// {{{
- const targetNode = e.target.closest('n2-treenode')
- if (targetNode.classList.contains('drag-source'))
- return
- e.stopPropagation()
- _app.dragIcon.icon('ok')
- this.classList.add('drag-target')
- }// }}}
- dragLeave(e) {// {{{
- e.stopPropagation()
- e.dataTransfer.dropEffect = 'none'
- e.dataTransfer.setDragImage(N2TreeNode.DRAG_ICON, -16, 8)
- _app.dragIcon.icon('')
- this.classList.remove('drag-target')
- }// }}}
-
- async expandNode(event) {// {{{
- const expanded = _app.sidebar.getNodeExpanded(this.node.UUID)
-
- if (event.shiftKey) {
- _app.sidebar.recursiveExpand(this.node, !expanded)
- } else {
- _app.sidebar.setNodeExpanded(this.node, !expanded)
- }
}// }}}
async fetchChildren(force_fetch) {//{{{
if (this.children_populated && !force_fetch)
@@ -679,8 +516,8 @@ export class N2TreeNode extends CustomHTMLElement {
if (this.rendered && force_update !== true)
return this
- if (this.sidebar.getNodeExpanded(this.node.UUID) || force_refetch_children)
- await this.fetchChildren(force_refetch_children)
+ if (this.sidebar.getNodeExpanded(this.node.UUID))
+ await this.fetchChildren()
// Update the name and selected status.
this.elName.querySelector('span').innerText = this.node.get('Name')
@@ -703,17 +540,6 @@ export class N2TreeNode extends CustomHTMLElement {
// The expand icon
is only changed to not get a flickering when re-rendering.
if (this.node.UUID === ROOT_NODE)
this.setImgSrc(this.elExpand, `/images/${window._VERSION}/icon_home.svg`)
-
- else if (this.node.UUID === DELETED_NODE) {
- this.setImgSrc(this.elExpand, `/images/${window._VERSION}/leaf_deleted.svg`)
- this.elExpand.classList.add('deleted')
- }
-
- else if (this.node.UUID === ORPHANED_NODE) {
- this.setImgSrc(this.elExpand, `/images/${window._VERSION}/leaf_orphaned.svg`)
- this.elExpand.classList.add('deleted')
- }
-
else if (!this.node.hasChildren())
this.setImgSrc(this.elExpand, `/images/${window._VERSION}/leaf.svg`)
else if (this.sidebar.getNodeExpanded(this.node.UUID))
@@ -748,24 +574,6 @@ export class N2TreeNode extends CustomHTMLElement {
img.setAttribute('src', newSrc)
}// }}}
}
-
-class SpecialNodeDeleted extends N2TreeNode {
- constructor(sidebar, node, parent) {//{{{
- super(sidebar, node, parent)
- this.removeAttribute('draggable')
- }//}}}
-}
-
-class SpecialNodeOrphaned extends N2TreeNode {
- constructor(sidebar, node, parent) {//{{{
- super(sidebar, node, parent)
- this.removeAttribute('draggable')
- }//}}}
-}
-
-customElements.define('n2-sidebar', N2Sidebar)
customElements.define('n2-treenode', N2TreeNode)
-customElements.define('n2-specialnodedeleted', SpecialNodeDeleted)
-customElements.define('n2-specialnodeorphaned', SpecialNodeOrphaned)
// vim: foldmethod=marker
diff --git a/static/js/sync.mjs b/static/js/sync.mjs
index daa603f..4db4473 100644
--- a/static/js/sync.mjs
+++ b/static/js/sync.mjs
@@ -19,8 +19,8 @@ export class Sync {
let nodeCountDownload = await this.getNodeCount(oldMax)
let nodeCountUpload = await nodeStore.sendQueue.count()
+ console.log(nodeCountUpload)
- _mbus.dispatch('SYNC_START')
_mbus.dispatch('SYNC_DOWNLOAD_COUNT', { count: nodeCountDownload })
_mbus.dispatch('SYNC_UPLOAD_COUNT', { count: nodeCountUpload })
@@ -81,16 +81,15 @@ export class Sync {
handled++
if (handled % 100 === 0)
- _mbus.dispatch('SYNC_DOWNLOADED', { handled })
+ _mbus.dispatch('SYNC_HANDLED', { handled })
}
} while (res.Continue)
- _mbus.dispatch('SYNC_DOWNLOADED', { handled })
+ _mbus.dispatch('SYNC_HANDLED', { handled })
nodeStore.setAppState('latest_sync_node', currMax)
} catch (e) {
- console.error('sync node tree', e)
- alert(e.message)
+ console.log('sync node tree', e)
} finally {
syncEnd = Date.now()
const duration = (syncEnd - syncStart) / 1000
@@ -158,8 +157,8 @@ export class Sync {
_mbus.dispatch('SYNC_UPLOADED', { count: nodesToSend.length })
} catch (e) {
- console.error(e)
- alert(e.message)
+ console.trace(e)
+ alert(e)
return
}
}
@@ -177,23 +176,21 @@ export class N2SyncProgress extends CustomHTMLElement {
}// }}}
constructor() {//{{{
super()
+
this.reset()
- _mbus.subscribe('SYNC_START', () => this.reset())
_mbus.subscribe('SYNC_DOWNLOAD_COUNT', event => this.progressHandler(event))
_mbus.subscribe('SYNC_UPLOAD_COUNT', event => this.progressHandler(event))
- _mbus.subscribe('SYNC_DOWNLOADED', event => this.progressHandler(event))
- _mbus.subscribe('SYNC_UPLOADED', event => this.progressHandler(event))
+ _mbus.subscribe('SYNC_HANDLED', event => this.progressHandler(event))
_mbus.subscribe('SYNC_DONE', event => this.progressHandler(event))
+ _mbus.subscribe('SYNC_UPLOADED', event => this.progressHandler(event))
}//}}}
reset() {//{{{
- this.classList.remove('ok')
this.state = {
nodesToDownload: 0,
nodesToUpload: 0,
- nodesDowloaded: 0,
+ nodesSynced: 0,
nodesUploaded: 0,
}
- this.render()
}//}}}
progressHandler(event) {//{{{
const eventData = event.detail.data
@@ -208,32 +205,31 @@ export class N2SyncProgress extends CustomHTMLElement {
this.setSyncState(true)
break
- case 'SYNC_DOWNLOADED':
- this.state.nodesDowloaded = eventData.handled
+ case 'SYNC_HANDLED':
+ console.log('SYNC_HANDLED', eventData.handled)
+ this.state.nodesSynced = eventData.handled
+ break
+
+ case 'SYNC_DONE':
+ // Hides the progress bar.
+ this.setSyncState(false)
+
+ // Don't update anything if nothing was synced.
+ if (this.state.nodesSynced === 0)
+ break
+
+ // Reload the tree nodes to reflect the new/updated nodes.
+ window._app.tree.reset()
break
case 'SYNC_UPLOADED':
this.state.nodesUploaded += eventData.count
break
-
- case 'SYNC_DONE':
- this.classList.add('ok')
-
- // Hides the progress bar.
- this.setSyncState(false)
-
- // Don't update anything if nothing was synced.
- if (this.state.nodesDowloaded === 0)
- break
-
- // Reload the tree nodes to reflect the new/updated nodes.
- window._app.sidebar.reset()
- break
}
this.render()
}//}}}
render() {//{{{
- this.elDownloadTransferred.innerText = this.state.nodesDowloaded
+ this.elDownloadTransferred.innerText = this.state.nodesSynced
this.elDownloadTotal.innerText = this.state.nodesToDownload
this.elUploadTransferred.innerText = this.state.nodesUploaded
@@ -243,7 +239,6 @@ export class N2SyncProgress extends CustomHTMLElement {
if (state)
this.classList.add('show')
else
- // Give the user a chance to see what it ended on.
setTimeout(() => this.classList.remove('show'), 1500)
}// }}}
}
diff --git a/user.go b/user.go
new file mode 100644
index 0000000..b1c2abf
--- /dev/null
+++ b/user.go
@@ -0,0 +1,27 @@
+package main
+
+import (
+ // External
+ "github.com/golang-jwt/jwt/v5"
+)
+
+type UserSession struct {
+ UserID int
+ Username string
+ Password string
+ Name string
+ ClientUUID string
+}
+
+func NewUser(claims jwt.MapClaims) (u UserSession) {
+ uid, _ := claims["uid"].(float64)
+ name, _ := claims["name"].(string)
+ username, _ := claims["login"].(string)
+ clientUUID, _ := claims["cid"].(string)
+
+ u.UserID = int(uid)
+ u.Username = username
+ u.Name = name
+ u.ClientUUID = clientUUID
+ return
+}
diff --git a/user/pkg.go b/user/pkg.go
deleted file mode 100644
index bcdfac8..0000000
--- a/user/pkg.go
+++ /dev/null
@@ -1,63 +0,0 @@
-package user
-
-import (
- // External
- "github.com/golang-jwt/jwt/v5"
- "github.com/jmoiron/sqlx"
-
- // Standard
- "encoding/json"
-)
-
-type User struct {
- ID int
- Username string
- Name string
- Preferences map[string]UserPreferences
-}
-
-type UserSession struct {
- UserID int
- Username string
- Password string
- Name string
- ClientUUID string
- Db *sqlx.DB
-}
-
-type UserPreferences struct {
- DownloadImages bool
- DownloadFiles bool
-}
-
-func NewUser(claims jwt.MapClaims) (u UserSession) {
- uid, _ := claims["uid"].(float64)
- name, _ := claims["name"].(string)
- username, _ := claims["login"].(string)
- clientUUID, _ := claims["cid"].(string)
-
- u.UserID = int(uid)
- u.Username = username
- u.Name = name
- u.ClientUUID = clientUUID
- return
-}
-
-func (u UserSession) Preferences() (prefs map[string]UserPreferences, err error) {
- row := u.Db.QueryRow(`SELECT preferences FROM public.user WHERE id=$1`, u.UserID)
-
- var data []byte
- err = row.Scan(&data)
- if err != nil {
- return
- }
-
- err = json.Unmarshal(data, &prefs)
- return
-}
-
-func (u UserSession) SetPreferences(prefs map[string]UserPreferences) (err error) {
- j, _ := json.Marshal(prefs)
- _, err = u.Db.Exec(`UPDATE public.user SET preferences=$2 WHERE id=$1`, u.UserID, j)
- return
-}
diff --git a/views/pages/notes2.gotmpl b/views/pages/notes2.gotmpl
index 2755aea..422b672 100644
--- a/views/pages/notes2.gotmpl
+++ b/views/pages/notes2.gotmpl
@@ -1,12 +1,6 @@
{{ define "page" }}
-
-
-
-
-
-
-
+
-
-
-

-
{{ .VERSION }}
-
-
Create note
-
-
-
-
-
-
-
+
+
+
+
+
+