diff --git a/.gitignore b/.gitignore index 7d5585e..1202234 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ notes2 -untracked diff --git a/main.go b/main.go index 8edd01c..2e938ca 100644 --- a/main.go +++ b/main.go @@ -23,7 +23,7 @@ import ( "text/template" ) -const VERSION = "v5" +const VERSION = "v3" const CONTEXT_USER = 1 const SYNC_PAGINATION = 200 @@ -129,6 +129,7 @@ func main() { // {{{ } http.HandleFunc("/", rootHandler) + http.HandleFunc("/notes2", pageNotes2) http.HandleFunc("/login", pageLogin) http.HandleFunc("/sync", pageSync) http.HandleFunc("/offline", pageOffline) @@ -188,13 +189,7 @@ func rootHandler(w http.ResponseWriter, r *http.Request) { // {{{ // All URLs not specifically handled are routed to this function. // Everything going here should be a static resource. if r.URL.Path == "/" { - page := NewPage("notes2") - - err := Webengine.Render(page, w, r) - if err != nil { - w.Write([]byte(err.Error())) - return - } + http.Redirect(w, r, "/notes2", http.StatusSeeOther) return } @@ -250,6 +245,15 @@ func pageLogin(w http.ResponseWriter, r *http.Request) { // {{{ return } } // }}} +func pageNotes2(w http.ResponseWriter, r *http.Request) { // {{{ + page := NewPage("notes2") + + err := Webengine.Render(page, w, r) + if err != nil { + w.Write([]byte(err.Error())) + return + } +} // }}} func pageSync(w http.ResponseWriter, r *http.Request) { // {{{ page := NewPage("sync") @@ -276,9 +280,9 @@ func actionSyncFromServer(w http.ResponseWriter, r *http.Request) { // {{{ } /* - Log.Debug("/sync/from_server", "num_nodes", len(nodes), "maxSeq", maxSeq) - foo, _ := json.Marshal(nodes) - os.WriteFile(fmt.Sprintf("/tmp/nodes-%d.json", offset), foo, 0644) + Log.Debug("/sync/from_server", "num_nodes", len(nodes), "maxSeq", maxSeq) + foo, _ := json.Marshal(nodes) + os.WriteFile(fmt.Sprintf("/tmp/nodes-%d.json", offset), foo, 0644) */ j, _ := json.Marshal(struct { diff --git a/static/css/notes2.css b/static/css/notes2.css index 8351858..71737dd 100644 --- a/static/css/notes2.css +++ b/static/css/notes2.css @@ -52,51 +52,51 @@ html { #tree { grid-area: tree; display: grid; - background-color: #ffffff; + background-color: #fafafa; color: #444; z-index: 100; - border-right: 2px solid #ddd; + border-right: 1px solid #ddd; + + n2-tree { + /*border: 2px solid #f8f8f8;*/ + padding: 16px 48px 16px 24px; + } + + &:focus-within { + n2-tree { + /* + border: 2px solid #fe5f55; + */ + } + + } + #logo { display: grid; - grid-template-columns: min-content 1fr min-content; - align-items: center; - justify-items: start; + position: relative; + justify-items: center; + margin-top: 8px; + margin-bottom: 8px; + margin-left: 24px; + margin-right: 24px; cursor: pointer; - padding: 16px; - border-bottom: 1px solid #ccc; - .el-search { - justify-self: end; - } + img { + width: 128px; + left: -20px; - img:first-child { - height: 24px; - margin-right: 8px; } } .icons { display: flex; justify-content: center; - margin: 16px 0px 32px 0px; + margin-bottom: 32px; gap: 8px; } - n2-tree { - .el-treenodes { - margin: 32px; - } - } - - &:focus-within { - n2-tree { - } - - } - - .node { display: grid; grid-template-columns: 40px min-content; @@ -145,6 +145,16 @@ html { } } +#tree-nodes { + padding: 16px 32px; + /* + border-radius: 8px; +*/ + /* + box-shadow: 5px 5px 10px -5px rgba(0, 0, 0, 0.75); + */ +} + #crumbs { grid-area: crumbs; display: grid; @@ -306,6 +316,7 @@ n2-nodeui { margin-bottom: 32px; &:invalid { + background: #f5f5f5; padding-top: 16px; } } diff --git a/static/favicon.ico b/static/favicon.ico deleted file mode 100644 index 299310f..0000000 Binary files a/static/favicon.ico and /dev/null differ diff --git a/static/images/collapsed.svg b/static/images/collapsed.svg index db06415..d93f4ca 100644 --- a/static/images/collapsed.svg +++ b/static/images/collapsed.svg @@ -8,7 +8,7 @@ version="1.1" id="svg1" sodipodi:docname="collapsed.svg" - inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)" + inkscape:version="1.4.2 (ebf0e94, 2025-05-08)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" @@ -23,13 +23,13 @@ inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:document-units="px" - inkscape:zoom="19.349237" - inkscape:cx="11.809251" - inkscape:cy="6.3051583" - inkscape:window-width="1093" - inkscape:window-height="1401" - inkscape:window-x="2560" - inkscape:window-y="0" + inkscape:zoom="4.8373092" + inkscape:cx="6.201795" + inkscape:cy="-12.40359" + inkscape:window-width="1916" + inkscape:window-height="1161" + inkscape:window-x="0" + inkscape:window-y="18" inkscape:window-maximized="1" inkscape:current-layer="layer1" showguides="false" /> @@ -42,13 +42,9 @@ transform="translate(-102.39375,-146.31458)">
${content}
\n` + return `${content}
\n` }, list(token) { @@ -134,7 +134,7 @@ export class MarkedPosition { }, listitem(token) { - return ``
+ return ``
+ (token.escaped ? code : escapeHtmlEntities(code, true))
+ '
\n'
}
- return `'
+ (token.escaped ? code : escapeHtmlEntities(code, true))
@@ -157,7 +157,7 @@ export class MarkedPosition {
blockquote(token) {
const body = this.parser.parse(token.tokens)
- return `\n${body}
\n`
+ return `\n${body}
\n`
},
html(token) {
@@ -169,11 +169,11 @@ export class MarkedPosition {
},
hr(token) {
- return `
\n`
+ return `
\n`
},
checkbox(token) {
- return ` '
},
@@ -218,7 +218,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="setpos(event)" data-offset-start="${start}" data-offset-end="${end}"`
+ ofs = `onclick="setpos(event)" data-offset-start="${start}" data-offset-end="${end}"`
}
const content = this.parser.parseInline(token.tokens);
@@ -230,23 +230,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) {
@@ -256,7 +256,7 @@ export class MarkedPosition {
return text
}
token.href = cleanHref
- let out = ' {
+ this.treeNodeComponents[node.UUID] = createRef()
+ return html`<${TreeNode} key=${`treenode_${node.UUID}`} tree=${this} node=${node} ref=${this.treeNodeComponents[node.UUID]} selected=${node.UUID === app.state.startNode?.UUID} />`
+ })
+
+ return html`
+
+ _notes2.current.goToNode(ROOT_NODE)}>
+
+
_mbus.dispatch('op-search')} />
+
_sync.run()} />
+
+ ${renderedTreeTrunk}
+ `
+ }//}}}
+ componentDidMount() {//{{{
+ //this.treeDiv.current.addEventListener('keydown', event => this.keyHandler(event))
+
+ // This will show and select the treenode that is selected in the node UI.
+ const node = _notes2.current?.nodeUI.current?.node.value
+ if (node === null)
+ return
+ _notes2.current.tree.expandToTrunk(node)
+ this.setSelected(node)
+ }//}}}
+
+ fetchChildrenNotify(uuid, fn) {//{{{
+ if (this.childrenFetchedCallbacks[uuid] === undefined)
+ this.childrenFetchedCallbacks[uuid] = [fn]
+ else
+ this.childrenFetchedCallbacks[uuid].push(fn)
+ }//}}}
+ fetchChildrenOn(uuid) {//{{{
+ if (this.childrenFetchedCallbacks[uuid] === undefined)
+ return
+ for (const fn of this.childrenFetchedCallbacks[uuid])
+ fn(uuid)
+ delete this.childrenFetchedCallbacks[uuid]
+ }//}}}
+
+ populateFirstLevel(callback = null) {//{{{
+ nodeStore.get(ROOT_NODE)
+ .then(node => node.fetchChildren())
+ .then(children => {
+ this.treeNodeComponents = {}
+ this.treeTrunk = []
+ for (const node of children) {
+ // The root node isn't supposed to be shown in the tree.
+ if (node.UUID === ROOT_NODE)
+ continue
+ if (node.ParentUUID === ROOT_NODE)
+ this.treeTrunk.push(node)
+ }
+ this.forceUpdate()
+ if (callback)
+ callback()
+
+ })
+ .catch(e => { console.log(e); console.log(e.type, e.error); alert(e.error) })
+ }//}}}
+ setSelected(node, dontExpand) {//{{{
+ // The previously selected node, if any, needs to be rerendered
+ // to not retain its 'selected' class.
+ const prevUUID = this.selectedNode?.UUID
+ this.selectedNode = node
+ if (prevUUID)
+ this.treeNodeComponents[prevUUID]?.current.forceUpdate()
+
+ // And now the newly selected node is rerendered.
+ this.treeNodeComponents[node.UUID]?.current.forceUpdate()
+
+ if (!dontExpand)
+ this.setNodeExpanded(node, true)
+ }//}}}
+ isSelected(node) {//{{{
+ return this.selectedNode?.UUID === node.UUID
+ }//}}}
+ async expandToTrunk(node) {//{{{
+ // Get all ancestors from a certain node up to the highest grandparent.
+ const ancestry = await nodeStore.getNodeAncestry(node, [])
+ for (const i in ancestry) {
+ await nodeStore.node(ancestry[i].UUID).fetchChildren()
+ this.setNodeExpanded(ancestry[i], true)
+ }
+
+ // Already a top node, no need to expand anything.
+ if (ancestry.length === 0)
+ return
+
+ // Start the chain of by expanding the top node.
+ this.setNodeExpanded(ancestry[ancestry.length - 1], true)
+ }//}}}
+ getNodeExpanded(UUID) {//{{{
+ if (this.expandedNodes[UUID] === undefined)
+ this.expandedNodes[UUID] = signal(false)
+ return this.expandedNodes[UUID].value
+ }//}}}
+ async setNodeExpanded(node, value) {//{{{
+ return new Promise((resolve, reject) => {
+ const work = uuid => {
+ // Creating a default value if it doesn't exist already.
+ this.getNodeExpanded(uuid)
+ this.expandedNodes[uuid].value = value
+ resolve()
+ }
+
+ if (node.hasFetchedChildren()) {
+ work(node.UUID)
+ return
+ } else {
+ this.fetchChildrenNotify(node.UUID, uuid => work(uuid))
+ }
+ })
+ }//}}}
+ getParentWithNextSibling(node) {//{{{
+ let currNode = node
+ while (currNode !== null && currNode.UUID !== ROOT_NODE && currNode.getSiblingAfter() === null) {
+ currNode = currNode.getParent()
+ }
+ return currNode?.getSiblingAfter()
+ }//}}}
+ getLastExpandedNode(node) {//{{{
+ let currNode = node
+ while (this.getNodeExpanded(currNode.UUID) && currNode.hasChildren()) {
+ currNode = currNode.Children[currNode.Children.length - 1]
+ }
+ return currNode
+ }//}}}
+
+ async recursiveExpand(node, state) {//{{{
+ if (state)
+ await this.setNodeExpanded(node, true)
+
+ for (const child of node.Children)
+ await this.recursiveExpand(child, state)
+
+ if (!state)
+ await this.setNodeExpanded(node, false)
+ }//}}}
+
+ async keyHandler(event) {//{{{
+ let handled = true
+ const n = this.selectedNode
+ const Space = ' '
+
+ // This handler would otherwise react to stuff like Ctrl+L.
+ if (event.ctrlKey || event.altKey)
+ return
+
+ switch (event.key) {
+ // Space and enter is toggling expansion.
+ // Holding shift down does it recursively.
+ case Space:
+ case 'Enter':
+ const expanded = this.getNodeExpanded(n.UUID)
+ if (event.shiftKey) {
+ this.recursiveExpand(n, !expanded)
+ } else {
+ this.setNodeExpanded(n, !expanded)
+ }
+ 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
+ }
+
+ if (handled) {
+ event.preventDefault()
+ event.stopPropagation()
+ }
+ }//}}}
+ async navigateLeft(n) {//{{{
+ if (n === null)
+ return
+
+ const expanded = this.getNodeExpanded(n.UUID)
+ if (expanded && n.hasChildren()) {
+ this.setNodeExpanded(n, false)
+ return
+ }
+
+ if (n.isFirstSibling() && n.getParent().UUID !== ROOT_NODE) {
+ await _notes2.current.goToNode(n.getParent()?.UUID, true, true)
+ return
+ }
+
+ const siblingBefore = n.getSiblingBefore()
+ const siblingExpanded = this.getNodeExpanded(siblingBefore?.UUID)
+ if (siblingBefore !== null && siblingExpanded && siblingBefore.hasChildren()) {
+ const siblingAbove = this.getLastExpandedNode(siblingBefore)
+ await _notes2.current.goToNode(siblingAbove?.UUID, true, true)
+ return
+ }
+
+ await _notes2.current.goToNode(n.getSiblingBefore()?.UUID, true, true)
+ }//}}}
+ async navigateRight(n) {//{{{
+ if (n === null)
+ return
+
+ const siblingAfter = n.getSiblingAfter()
+ const expanded = this.getNodeExpanded(n.UUID)
+
+ if (!expanded && n.hasChildren()) {
+ this.setNodeExpanded(n, true)
+ return
+ }
+
+ if (expanded && n.hasChildren()) {
+ await _notes2.current.goToNode(n.Children[0]?.UUID, true, true)
+ return
+ }
+
+ if (n.isLastSibling()) {
+ const nextNode = this.getParentWithNextSibling(n)
+ await _notes2.current.goToNode(nextNode?.UUID, true, true)
+ return
+ }
+
+ await _notes2.current.goToNode(n.getSiblingAfter()?.UUID, true, true)
+ }//}}}
+ async navigateUp(n) {//{{{
+ if (n === null)
+ return
+
+ let parent = null
+ const siblingBefore = n.getSiblingBefore()
+ let siblingExpanded = false
+ if (siblingBefore !== null)
+ siblingExpanded = this.getNodeExpanded(siblingBefore.UUID)
+
+ if (n.isFirstSibling()) {
+ parent = n.getParent()
+ if (parent?.UUID === ROOT_NODE)
+ return
+ await _notes2.current.goToNode(parent?.UUID, true, true)
+ return
+ }
+
+ if (siblingBefore !== null && siblingExpanded && siblingBefore.hasChildren()) {
+ await _notes2.current.goToNode(siblingBefore.Children[siblingBefore.Children.length - 1]?.UUID, true, true)
+ return
+ }
+
+ if (siblingBefore) {
+ await _notes2.current.goToNode(siblingBefore.UUID, true, true)
+ return
+ }
+ }//}}}
+ async navigateDown(n) {//{{{
+ if (n === null)
+ return
+
+ const nodeExpanded = this.getNodeExpanded(n.UUID)
+
+ // Last node, not expanded, so it matters not whether it has children or not.
+ // Traverse upward to nearest parent with next sibling.
+ if (!nodeExpanded && n.isLastSibling()) {
+ const wantedNode = this.getParentWithNextSibling(n)
+ await _notes2.current.goToNode(wantedNode?.UUID, true, true)
+ return
+ }
+
+ if (nodeExpanded && n.isLastSibling() && !n.hasChildren()) {
+ const wantedNode = this.getParentWithNextSibling(n)
+ await _notes2.current.goToNode(wantedNode?.UUID, true, true)
+ return
+ }
+
+ // Node not expanded. Go to this node's next sibling.
+ // GoToNode will abort if given null.
+ if (!nodeExpanded || !n.hasChildren()) {
+ await _notes2.current.goToNode(n.getSiblingAfter()?.UUID, true, true)
+ return
+ }
+
+ // Node is expanded.
+ // Children will be visually beneath this node, if any.
+ if (nodeExpanded && n.hasChildren()) {
+ await _notes2.current.goToNode(n.Children[0].UUID, true, true)
+ return
+ }
+ }//}}}
+ async navigateTop() {//{{{
+ const root = await nodeStore.get(ROOT_NODE)
+ if (root.Children.length === 0)
+ return
+ await _notes2.current.goToNode(root.Children[0]?.UUID, true, true)
+ }//}}}
+ async navigateBottom() {//{{{
+ const root = await nodeStore.get(ROOT_NODE)
+ if (root.Children.length === 0)
+ return
+
+ const toplevel = root.Children[root.Children.length - 1]
+ const toplevelExpanded = this.getNodeExpanded(toplevel?.UUID)
+
+ if (toplevelExpanded) {
+ const lastnode = this.getLastExpandedNode(toplevel)
+ await _notes2.current.goToNode(lastnode?.UUID, true, true)
+ } else
+ await _notes2.current.goToNode(root.Children[root.Children.length - 1]?.UUID, true, true)
+ }//}}}
+}
+
+class TreeNode extends Component {
+ constructor(props) {//{{{
+ super(props)
+ this.children_populated = signal(false)
+ if (this.props.node.Level === 0 || this.props.tree.getNodeExpanded(this.props.node.UUID))
+ this.fetchChildren()
+ }//}}}
+ render({ tree, node, parent }) {//{{{
+ // Fetch the next level of children if the parent tree node is expanded and our children thus will be visible.
+ const selected = tree.isSelected(node) ? 'selected' : ''
+
+ if (!this.children_populated.value && tree.getNodeExpanded(parent?.props.node.UUID))
+ this.fetchChildren()
+
+ const children = node.Children.map(node => {
+ tree.treeNodeComponents[node.UUID] = createRef()
+ return html`<${TreeNode} key=${`treenode_${node.UUID}`} tree=${tree} node=${node} parent=${this} ref=${tree.treeNodeComponents[node.UUID]} selected=${node.UUID === tree.props.app.startNode?.UUID} />`
+ })
+
+ let expandImg = ''
+ if (node.Children.length === 0)
+ expandImg = html`
`
+ else {
+ if (tree.getNodeExpanded(node.UUID))
+ expandImg = html`
`
+ else
+ expandImg = html`
`
+ }
+
+ return html`
+
+
+ window._notes2.current.goToNode(node.UUID)}>${node.get('Name')}
+
+ `
+ }//}}}
+ async fetchChildren() {//{{{
+ await this.props.node.fetchChildren()
+ this.children_populated.value = true
+ }//}}}
+}
+
class Op {
constructor(id) {
this.id = id
diff --git a/static/js/tree.mjs b/static/js/tree.mjs
index b672742..3732fc5 100644
--- a/static/js/tree.mjs
+++ b/static/js/tree.mjs
@@ -1,19 +1,14 @@
import { ROOT_NODE } from 'node_store'
import { CustomHTMLElement } from './lib/custom_html_element.mjs'
-import { Color, Solver } from './lib/css_colorize.mjs'
export class N2Tree extends CustomHTMLElement {
static {// {{{
this.tmpl = document.createElement('template')
this.tmpl.innerHTML = `
-
-
-
-
-
+
-
-
+
+
`
@@ -36,7 +31,7 @@ export class N2Tree extends CustomHTMLElement {
this.elSync.addEventListener('click', () => _sync.run())
this.elLogo.addEventListener('click', () => _app.goToNode(ROOT_NODE, false, false))
- _mbus.subscribe('NODE_MODIFIED', ({ detail }) => {
+ _mbus.subscribe('NODE_MODIFIED', ({ detail })=>{
const node = detail.data.node
const treenode = this.treeNodeComponents[node.get('UUID')]
@@ -48,12 +43,6 @@ export class N2Tree extends CustomHTMLElement {
})
this.populateFirstLevel()
-
- /* XXX - set color */
- let color = new Color(255, 96, 80)
- let solver = new Solver(color)
- let result = solver.solve()
- this.elSettings.style.filter = result.filter
}// }}}
render() {// {{{
if (this.rendered)
diff --git a/static/service_worker.js b/static/service_worker.js
index 1717eef..806eaad 100644
--- a/static/service_worker.js
+++ b/static/service_worker.js
@@ -1,6 +1,7 @@
const CACHE_NAME = 'notes2-{{ .VERSION }}'
const CACHED_ASSETS = [
'/',
+ '/notes2',
'/offline',
'/css/{{ .VERSION }}/main.css',
@@ -24,6 +25,7 @@ const CACHED_ASSETS = [
'/js/{{ .VERSION }}/crypto.mjs',
'/js/{{ .VERSION }}/key.mjs',
'/js/{{ .VERSION }}/lib/custom_html_element.mjs',
+ '/js/{{ .VERSION }}/lib/fullcalendar.min.js',
'/js/{{ .VERSION }}/lib/node_modules/marked/lib/marked.esm.js',
'/js/{{ .VERSION }}/lib/node_modules/marked-token-position/lib/index.esm.js',
'/js/{{ .VERSION }}/lib/sjcl.js',
diff --git a/views/layouts/main.gotmpl b/views/layouts/main.gotmpl
index c744166..8ffa278 100644
--- a/views/layouts/main.gotmpl
+++ b/views/layouts/main.gotmpl
@@ -16,6 +16,10 @@
"node_store": "/js/{{ .VERSION }}/node_store.mjs",
"node": "/js/{{ .VERSION }}/node.mjs",
"tree": "/js/{{ .VERSION }}/tree.mjs"
+ {{/*
+ "session": "/js/{{ .VERSION }}/session.mjs",
+ "ws": "/_js/{{ .VERSION }}/websocket.mjs"
+ */}}
}
}
@@ -29,6 +33,8 @@
import { MessageBus } from '/js/{{ .VERSION }}/mbus.mjs'
window._mbus = new MessageBus()
+
+
{{ block "page" . }}{{ end }}
diff --git a/views/pages/login.gotmpl b/views/pages/login.gotmpl
index 3e2235f..3f4406e 100644
--- a/views/pages/login.gotmpl
+++ b/views/pages/login.gotmpl
@@ -29,7 +29,7 @@ class Login {
const password = document.getElementById('password').value
API.authenticate(username, password)
.then(ans=>{
- location.href = '/'
+ location.href = '/notes2'
})
.catch(e=>{
setTimeout(()=>this.errorDiv.innerText = e, 75)