diff --git a/main.go b/main.go
index e608601..a9ac728 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 = 100
var (
FlagGenerate bool
@@ -269,11 +269,9 @@ func actionSyncFromServer(w http.ResponseWriter, r *http.Request) { // {{{
return
}
- /*
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 {
OK bool
@@ -290,6 +288,7 @@ func actionSyncFromServerCount(w http.ResponseWriter, r *http.Request) { // {{{
user := getUser(r)
changedFrom, _ := strconv.Atoi(r.PathValue("sequence"))
+ Log.Debug("FOO", "UUID", user.ClientUUID, "changedFrom", changedFrom)
count, err := NodesCount(user.UserID, uint64(changedFrom), user.ClientUUID)
if err != nil {
Log.Error("/sync/from_server/count", "error", err)
@@ -335,14 +334,9 @@ func actionSyncToServer(w http.ResponseWriter, r *http.Request) { // {{{
return
}
- _, err = db.Exec(`CALL add_nodes($1, $2, $3::jsonb)`, user.UserID, user.ClientUUID, request.NodeData)
- if err != nil {
- Log.Error("sync", "error", err)
- httpError(w, err)
- return
- }
+ db.Exec(`CALL add_nodes($1, $2, $3::jsonb)`, user.UserID, user.ClientUUID, request.NodeData)
- responseData(w, map[string]any{
+ responseData(w, map[string]interface{}{
"OK": true,
})
} // }}}
diff --git a/sql/00006.sql b/sql/00006.sql
index 453b260..6b0ea9b 100644
--- a/sql/00006.sql
+++ b/sql/00006.sql
@@ -1 +1,16 @@
-DROP INDEX public.node_uuid_idx;
+CREATE TABLE public.node_history (
+ id serial4 NOT NULL,
+ user_id int4 NOT NULL,
+ uuid bpchar(36) NOT NULL,
+ parents varchar[] NULL,
+ created timestamptz NOT NULL,
+ updated timestamptz NOT NULL,
+ name varchar(256) NOT NULL,
+ "content" text NOT NULL,
+ content_encrypted text NOT NULL,
+ markdown bool DEFAULT false NOT NULL,
+ client bpchar(36) DEFAULT ''::bpchar NOT NULL,
+ CONSTRAINT node_history_pk PRIMARY KEY (id),
+ CONSTRAINT node_history_user_fk FOREIGN KEY (user_id) REFERENCES public."user"(id) ON DELETE RESTRICT ON UPDATE RESTRICT
+);
+CREATE INDEX node_history_uuid_idx ON public.node USING btree (uuid);
diff --git a/static/css/login.css b/static/css/login.css
index 7e19cb8..88a9140 100644
--- a/static/css/login.css
+++ b/static/css/login.css
@@ -1,44 +1,37 @@
-@import "theme.css";
-
#app {
- display: grid;
- justify-items: center;
- margin-top: 128px;
+ display: grid;
+ justify-items: center;
+ margin-top: 128px;
}
-
#logo {
- margin-bottom: 48px;
+ margin-bottom: 48px;
}
-
#box {
- display: grid;
- grid-gap: 16px 0;
- justify-items: center;
- width: 300px;
- padding: 48px 0px;
- background-color: #fff;
- box-shadow: 0px 20px 52px -33px rgba(0,0,0,0.75);
- border-left: 8px solid var(--color3);
-
- input {
- padding: 4px 8px;
- font-size: 1em;
- width: calc(100% - 64px);
- border: 1px solid #aaa;
- border-radius: 4px;
- }
-
- button {
- padding: 6px 16px;
- font-size: 1em;
- border-radius: 4px;
- border: none;
- background-color: var(--color1);
- color: #fff;
- }
-
- #error {
- color: #c33;
- margin-top: 16px;
- }
+ display: grid;
+ grid-gap: 16px 0;
+ justify-items: center;
+ width: 300px;
+ padding: 48px 0px;
+ background-color: #fff;
+ box-shadow: 0px 20px 52px -33px rgba(0, 0, 0, 0.75);
+ border-left: 8px solid #666;
+}
+#box input {
+ padding: 4px 8px;
+ font-size: 1em;
+ width: calc(100% - 64px);
+ border: 1px solid #aaa;
+ border-radius: 4px;
+}
+#box button {
+ padding: 6px 16px;
+ font-size: 1em;
+ border-radius: 4px;
+ border: none;
+ background-color: #fe5f55;
+ color: #fff;
+}
+#box #error {
+ color: #c33;
+ margin-top: 16px;
}
diff --git a/static/css/main.css b/static/css/main.css
index a8924d9..75f1925 100644
--- a/static/css/main.css
+++ b/static/css/main.css
@@ -1,29 +1,23 @@
-@import "theme.css";
-
html {
- box-sizing: border-box;
- background: var(--color2);
- font-family: "Liberation Mono", monospace;
- font-size: 14px;
- margin: 0px;
- padding: 0px;
+ box-sizing: border-box;
+ background: #efede8;
+ font-family: "Liberation Mono", monospace;
+ font-size: 14px;
+ margin: 0px;
+ padding: 0px;
}
-
body {
- margin: 0px;
- padding: 0px;
+ margin: 0px;
+ padding: 0px;
}
-
*,
*:before,
*:after {
- box-sizing: inherit;
+ box-sizing: inherit;
}
-
*:focus {
- outline: none;
+ outline: none;
}
-
[onClick] {
- cursor: pointer;
+ cursor: pointer;
}
diff --git a/static/css/notes2.css b/static/css/notes2.css
index adfa51d..b6b0963 100644
--- a/static/css/notes2.css
+++ b/static/css/notes2.css
@@ -1,382 +1,264 @@
-@import "theme.css";
-
html {
- background-color: #fff;
+ 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;
- }
- }
+ min-height: 100vh;
+ display: grid;
+ grid-template-areas: "tree crumbs" "tree sync" "tree name" "tree content" "tree blank";
+ grid-template-columns: min-content 1fr;
+ grid-template-rows: 48px 56px 48px min-content 1fr;
+}
+@media only screen and (max-width: 600px) {
+ #notes2 {
+ grid-template-areas: "crumbs" "sync" "name" "content" "blank";
+ grid-template-columns: 1fr;
+ }
+ #notes2 #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: var(--color1);
- }
-
- &.selected {
- color: var(--color1);
- font-weight: bold;
- }
-
- }
-
- .children {
- padding-left: 24px;
- margin-left: 8px;
- border-left: 1px solid #444;
- grid-column: 1 / -1;
-
- &.collapsed {
- display: none;
- }
- }
- }
+ grid-area: tree;
+ padding: 16px 32px;
+ background-color: #333;
+ color: #ddd;
+ z-index: 100;
+ border-left: 2px solid #333;
}
-
-#tree-nodes {
- padding: 16px 32px;
- background-color: #333;
- border-radius: 8px;
- box-shadow: 5px 5px 10px -5px rgba(0, 0, 0, 0.75);
+#tree:focus {
+ border-left: 2px solid #FE5F55;
+}
+#tree #logo {
+ display: grid;
+ position: relative;
+ justify-items: center;
+ margin-bottom: 8px;
+ margin-left: 24px;
+ margin-right: 24px;
+}
+#tree #logo img {
+ width: 128px;
+ left: -20px;
+}
+#tree .icons {
+ display: flex;
+ justify-content: center;
+ margin-bottom: 32px;
+ gap: 8px;
+}
+#tree .node {
+ display: grid;
+ grid-template-columns: 24px min-content;
+ grid-template-rows: min-content 1fr;
+ margin-top: 12px;
+}
+#tree .node .expand-toggle {
+ user-select: none;
+}
+#tree .node .expand-toggle img {
+ width: 16px;
+ height: 16px;
+}
+#tree .node .name {
+ white-space: nowrap;
+ cursor: pointer;
+ user-select: none;
+}
+#tree .node .name:hover {
+ color: #fe5f55;
+}
+#tree .node .name.selected {
+ color: #fe5f55;
+ font-weight: bold;
+}
+#tree .node .children {
+ padding-left: 24px;
+ margin-left: 8px;
+ border-left: 1px solid #444;
+ grid-column: 1 / -1;
+}
+#tree .node .children.collapsed {
+ display: none;
}
-
#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: var(--color1);
- color: var(--color2);
-
- .crumb:after {
- color: var(--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: var(--color1)
- }
-
- .crumb:last-child {
- margin-right: 0;
- }
-
- .crumb:last-child:after {
- content: '';
- margin-left: 0px;
- }
-
- }
-
+ grid-area: crumbs;
+ display: grid;
+ align-items: start;
+ justify-items: center;
+ margin: 0px 16px;
+}
+#crumbs .crumbs {
+ display: flex;
+ flex-wrap: wrap;
+ padding: 8px 16px;
+ background: #e4e4e4;
+ color: #333;
+ border-bottom-left-radius: 5px;
+ border-bottom-right-radius: 5px;
+}
+#crumbs .crumbs.node-modified {
+ background-color: #fe5f55;
+ color: #efede8;
+}
+#crumbs .crumbs.node-modified .crumb:after {
+ color: #efede8;
+}
+#crumbs .crumbs .crumb {
+ margin-right: 8px;
+ cursor: pointer;
+ user-select: none;
+ -webkit-tap-highlight-color: transparent;
+}
+#crumbs .crumbs .crumb:after {
+ content: "•";
+ margin-left: 8px;
+ color: #fe5f55;
+}
+#crumbs .crumbs .crumb:last-child {
+ margin-right: 0;
+}
+#crumbs .crumbs .crumb:last-child:after {
+ content: '';
+ margin-left: 0px;
}
-
#sync-progress {
- --radius: 8px;
-
- grid-area: sync;
- display: grid;
- justify-items: center;
- align-items: center;
-
- width: 100%;
- height: 56px;
-
- .container {
- position: relative;
-
- 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);
- }
- }
-
-
-
- &.hidden {
- visibility: hidden;
- opacity: 0;
- transition: visibility 0s 500ms, opacity 500ms linear;
- }
-
+ grid-area: sync;
+ display: grid;
+ justify-items: center;
+ width: 100%;
+ height: 56px;
+ position: relative;
+}
+#sync-progress progress {
+ width: 100%;
+ padding: 0 7px;
+ max-width: 900px;
+ height: 16px;
+ border-radius: 4px;
+}
+#sync-progress progress[value]::-webkit-progress-bar {
+ background-color: #eee;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25) inset;
+ border-radius: 4px;
+}
+#sync-progress progress[value]::-moz-progress-bar {
+ background-color: #eee;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25) inset;
+ border-radius: 4px;
+}
+#sync-progress progress[value]::-webkit-progress-value {
+ background: #ba5f59;
+ background: linear-gradient(180deg, #ba5f59 0%, #fe5f55 50%, #ba5f59 100%);
+ border-radius: 4px;
+}
+#sync-progress progress[value]::-moz-progress-value {
+ background: #ba5f59;
+ background: linear-gradient(180deg, #ba5f59 0%, #fe5f55 50%, #ba5f59 100%);
+ border-radius: 4px;
+}
+#sync-progress .count {
+ width: min-content;
+ white-space: nowrap;
+ margin-top: 0px;
+ color: #888;
+ position: absolute;
+ top: 22px;
+}
+#sync-progress.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;
+ 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;
+ /* 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: 1em;
}
-
.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;
+ /* 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 {
+ /* 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 > textarea,
.grow-wrap::after {
- /* Identical styling required!! */
- padding: 0.5rem;
- font: inherit;
-
- /* Place on top of each other */
- grid-area: 1 / 1 / 2 / 2;
+ /* 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;
- }
+ 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;
+}
+#node-content:invalid {
+ background: #f5f5f5;
+ padding-top: 16px;
}
-
#blank {
- grid-area: blank;
- height: 32px;
+ 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;
- }
- }
-
+dialog.op::backdrop {
+ background: rgba(0, 0, 0, 0.5);
}
-
-#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: "";
- }
- }
- }
+dialog.op .header {
+ font-weight: bold;
+ margin-top: 16px;
+}
+dialog.op .header:first-child {
+ margin-top: 0px;
+}
+#op-search .results {
+ display: grid;
+ grid-template-columns: min-content min-content;
+ grid-gap: 6px 16px;
+}
+#op-search .results div {
+ white-space: nowrap;
+}
+#op-search .results .ancestors {
+ display: flex;
+}
+#op-search .results .ancestors .ancestor::after {
+ content: ">";
+ margin: 0px 8px;
+ color: #a00;
+}
+#op-search .results .ancestors .ancestor:last-child::after {
+ content: "";
}
diff --git a/static/css/theme.css b/static/css/theme.css
index b9c47ed..e69de29 100644
--- a/static/css/theme.css
+++ b/static/css/theme.css
@@ -1,5 +0,0 @@
-:root {
- --color1: #fe5f55;
- --color2: #efede8;
- --color3: #666;
-}
diff --git a/static/js/app.mjs b/static/js/app.mjs
deleted file mode 100644
index 7926368..0000000
--- a/static/js/app.mjs
+++ /dev/null
@@ -1,316 +0,0 @@
-import { ROOT_NODE } from 'node_store'
-import { TreeNative } from 'tree'
-import { NodeUINative, Node } from 'node'
-import { SyncProgress } from 'sync'
-
-export class App {
- constructor() {// {{{
- this.currentNode = null
- this.treeNative = new TreeNative()
- this.crumbs = new Crumbs()
- this.crumbsElement = document.getElementById('crumbs')
- this.nodeUI = new NodeUINative(document.getElementById('note'))
-
- _mbus.subscribe('TREE_TRUNK_FETCHED', async () => {
- document.getElementById('tree').append(this.treeNative.render())
- document.getElementById('tree-nodes')?.focus()
-
- const startNode = await this.getStartNode()
- this.goToNode(startNode.UUID, false, false)
- })
-
- _mbus.subscribe('TREE_NODE_SELECTED', event => {
- const node = event.detail.data
- this.goToNode(node.UUID, false, false)
- })
-
- _mbus.subscribe('GO_TO_NODE', event => {
- const node = event.detail.data
- this.goToNode(node.nodeUUID, node.dontPush, node.dontExpand)
- })
-
- window.addEventListener('keydown', event => this.keyHandler(event))
- document.getElementById('notes2').addEventListener('click', event => {
- if (event.target.id === 'notes2')
- document.getElementById('node-content')?.focus()
- })
-
- const syncProgress = document.getElementById('sync-progress')
- new SyncProgress(syncProgress)
-
- window._sync = new Sync()
- window._sync.run()
- }// }}}
-
- keyHandler(event) {//{{{
- 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 (!(event.shiftKey && event.altKey) && !(event.key.toUpperCase() === 'S' && event.ctrlKey))
- return
-
- switch (event.key.toUpperCase()) {
- case 'T':
- if (document.activeElement.id === 'tree-nodes')
- document.getElementById('node-content').focus()
- else
- document.getElementById('tree-nodes').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':
- 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) {
- event.preventDefault()
- event.stopPropagation()
- }
- }//}}}
- async getStartNode() {//{{{
- let nodeUUID = ROOT_NODE
-
- // Is a UUID provided on the URI as an anchor?
- const parts = document.URL.split('#')
- if (parts[1]?.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i))
- nodeUUID = parts[1]
-
- return await nodeStore.get(nodeUUID)
- }//}}}
- async saveNode() {//{{{
- if (!this.currentNode.isModified())
- 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.currentNode
-
- // 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(node)
-
- // Updated node is saved to the primary node store.
- const nodeStoreAdding = nodeStore.add([node])
-
- await Promise.all([history, sendQueue, nodeStoreAdding])
- }//}}}
- async createNode() {//{{{
- let name = prompt("Name")
- if (!name)
- return
-
- const nn = Node.create(name, this.currentNode.UUID)
- nn.save()
-
- nodeStore.sendQueue.add(nn)
- nodeStore.add([nn])
-
- }//}}}
- async goToNode(nodeUUID, dontPush, dontExpand) {//{{{
- if (nodeUUID === null || nodeUUID === undefined)
- return
-
- // Don't switch notes until saved.
- if (this.nodeUI.isModified()) {
- if (!confirm("Changes not saved. Do you want to discard changes?"))
- return
- }
-
- if (!dontPush)
- history.pushState({ nodeUUID }, '', `/notes2#${nodeUUID}`)
-
- const node = nodeStore.node(nodeUUID)
-
- node.reset() // any modifications are discarded.
-
- this.currentNode = node
- this.treeNative.setSelected(node, dontExpand)
-
- const ancestors = await nodeStore.getNodeAncestry(node)
- _mbus.dispatch('CRUMBS_SET', ancestors, () => this.crumbsElement.replaceChildren(this.crumbs.render()))
- _mbus.dispatch('NODE_UI_OPEN', node)
- _mbus.dispatch('NODE_UNMODIFIED')
-
- // Scrolls node into view.
- this.treeNative.makeVisible(node)
- }//}}}
-}
-
-class Crumbs {
- constructor() {// {{{
- this.crumbs = []
- this.crumbsDiv = document.createElement('div')
- this.crumbsDiv.classList.add('crumbs')
-
- _mbus.subscribe('CRUMBS_SET', event => {
- this.crumbs = event.detail.data
- })
- }// }}}
- render() {// {{{
- const crumbs = this.crumbs.map(node =>
- (new Crumb(
- node.get('Name'),
- node.UUID,
- )).render()
- )
-
- const start = (new Crumb('Start', ROOT_NODE)).render()
- crumbs.push(start)
-
- this.crumbsDiv.replaceChildren(...crumbs.reverse())
- return this.crumbsDiv
- }// }}}
-}
-
-class Crumb {
- constructor(label, uuid) {// {{{
- this.label = label
- this.uuid = uuid
- }// }}}
- render() {// {{{
- const crumb = document.createElement('div')
- crumb.classList.add('crumb')
-
- const link = document.createElement('a')
- link.href = `/notes2#${this.uuid}`
- link.innerText = this.label
-
- crumb.appendChild(link)
- return crumb
-
- }// }}}
-}
-
-function tmpl(html) {// {{{
- const el = document.createElement('template')
- el.innerHTML = html
- return el.content.children
-}// }}}
-
-class Op {
- constructor(id) {// {{{
- this.id = id
- _mbus.subscribe(this.id, p => this.render(p))
- }// }}}
- render(html) {// {{{
- const op = document.getElementById('op')
- const t = document.createElement('template')
- t.innerHTML = `${html} `
- op.replaceChildren(t.content)
- document.getElementById(this.id).showModal()
- }// }}}
- get(selector) {// {{{
- return document.querySelector(`#${this.id} ${selector}`)
- }// }}}
- bind(selector, event, fn) {// {{{
- this.get(selector).addEventListener(event, evt => fn(evt))
- }// }}}
-}
-
-class OpSearch extends Op {
- constructor() {// {{{
- super('op-search')
- }// }}}
- render() {// {{{
- super.render(`
-
-
-
-
-
-
- `)
-
- this.bind('input[type="text"]', 'keydown', evt => this.search(evt))
- }// }}}
- search(event) {// {{{
- if (event.key !== 'Enter')
- return
-
- const searchFor = document.querySelector('#op-search input').value
- nodeStore.search(searchFor, ROOT_NODE)
- .then(res => this.displayResults(res))
- }// }}}
- displayResults(results) {// {{{
- const rs = []
- for (const r of results) {
- const ancestors = r.ancestry.reverse().map(a => {
- const div = tmpl(`${a.data.Name}
`)
- div[0].addEventListener('click', () => _notes2.current.goToNode(a.UUID))
- return div[0]
- })
-
-
- const div = tmpl(`${r.name}
`)
- div[0].addEventListener('click', () => _notes2.current.goToNode(r.uuid))
- rs.push(...div)
-
- const ancDev = tmpl('
')
- ancDev[0].append(...ancestors)
- rs.push(ancDev[0])
- }
- this.get('.results').replaceChildren(...rs)
- }// }}}
-}
-
-// vim: foldmethod=marker
diff --git a/static/js/lib/custom_html_element.mjs b/static/js/lib/custom_html_element.mjs
deleted file mode 100644
index dedb5d8..0000000
--- a/static/js/lib/custom_html_element.mjs
+++ /dev/null
@@ -1,57 +0,0 @@
-export class CustomHTMLElement extends HTMLElement {
- constructor() {// {{{
- super()
-
- this.appendChild(this.constructor.tmpl.content.cloneNode(true))
-
- this.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('-', ''))
- }// }}}
-}
-
-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/mbus.mjs b/static/js/mbus.mjs
index 0daa63c..da5f098 100644
--- a/static/js/mbus.mjs
+++ b/static/js/mbus.mjs
@@ -1,48 +1,17 @@
export class MessageBus {
constructor() {
- this.log = false
this.bus = new EventTarget()
}
subscribe(eventName, fn) {
- if (this.log) {
- console.groupCollapsed('MBUS subscribe - ', eventName);
- console.trace(); // hidden in collapsed group
- console.groupEnd();
- }
-
- this.bus.addEventListener(eventName, event=>{
- fn(event)
- if (event.detail.callback !== undefined)
- event.detail.callback(event)
- })
+ this.bus.addEventListener(eventName, fn)
}
unsubscribe(eventName, fn) {
- if (this.log) {
- console.groupCollapsed('MBUS unsubscribe - ', eventName);
- console.trace(); // hidden in collapsed group
- console.groupEnd();
- }
-
this.bus.removeEventListener(eventName, fn)
}
- dispatch(eventName, data, callback) {
- if (this.log) {
- console.groupCollapsed('MBUS dispatch - ', eventName);
- console.log('data', data);
- console.trace(); // hidden in collapsed group
- console.groupEnd();
- }
-
- const event = new CustomEvent(eventName, {
- detail: {
- data,
- callback,
- }
- })
-
- this.bus.dispatchEvent(event)
+ dispatch(eventName, data) {
+ this.bus.dispatchEvent(new CustomEvent(eventName, { detail: data }))
}
}
diff --git a/static/js/node.mjs b/static/js/node.mjs
index 3820941..92c8879 100644
--- a/static/js/node.mjs
+++ b/static/js/node.mjs
@@ -54,7 +54,7 @@ export class NodeUI extends Component {
${crumbDivs}
-
+ <${SyncProgress} ref=${this.syncProgress} />
${node.get('Name')}
<${NodeContent} key=${node.UUID} node=${node} ref=${this.nodeContent} />
@@ -156,12 +156,8 @@ export class NodeUI extends Component {
`
}//}}}
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
@@ -335,83 +331,16 @@ class NodeContent extends Component {
}//}}}
}
-export class NodeUINative {
- constructor(parentElement) {// {{{
- this.node = null
- this.parent = parentElement
- this.parent.replaceChildren(this.createElements())
-
- _mbus.subscribe('NODE_UI_OPEN', event => {
- this.node = event.detail.data
- this.render()
- })
-
- _mbus.subscribe('NODE_MODIFIED', () => {
- document.querySelector('#crumbs .crumbs')?.classList.add('node-modified')
- })
-
- _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
- }// }}}
- 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
- }// }}}
-
- 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()
- }// }}}
-
-}
-
export class Node {
static sort(a, b) {//{{{
if (a.data.Name < b.data.Name) return -1
if (a.data.Name > b.data.Name) return 0
return 0
}//}}}
- static create(name, parentUUID) {
- return new Node({
- UUID: uuidv7(),
- Created: (new Date()).toISOString(),
- Content: '',
- Name: name,
- ParentUUID: parentUUID,
- Markdown: false,
- History: false,
- })
- }
-
constructor(nodeData, level) {//{{{
-
this.Level = level
this.data = nodeData
+
this.UUID = nodeData.UUID
// Toplevel nodes are normalized to have the ROOT_NODE as parent.
@@ -425,12 +354,13 @@ export class Node {
this.Children = []
this.Ancestors = []
+ this._content = this.data.Content
+ this._modified = false
+
this._sibling_before = null
this._sibling_after = null
this._parent = null
- this.reset()
-
/*
this.RenderMarkdown = signal(nodeData.RenderMarkdown)
this.Markdown = false
@@ -447,10 +377,6 @@ export class Node {
*/
}//}}}
- reset() {// {{{
- this._content = this.data.Content
- this._modified = false
- }// }}}
get(prop) {//{{{
return this.data[prop]
}//}}}
@@ -458,9 +384,6 @@ export class Node {
// '2024-12-17T17:33:48.85939Z
return new Date(Date.parse(this.data.Updated))
}//}}}
- isModified() {// {{{
- return this._modified
- }// }}}
hasFetchedChildren() {//{{{
return this._children_fetched
}//}}}
@@ -485,8 +408,7 @@ export class Node {
}
// Notify the tree that all children are fetched and ready to process.
- //_notes2.current.tree.fetchChildrenOn(this.UUID)
- _mbus.dispatch(`NODE_CHILDREN_FETCHED_${this.UUID}`)
+ _notes2.current.tree.fetchChildrenOn(this.UUID)
return this.Children
}//}}}
@@ -513,12 +435,12 @@ export class Node {
if (this.CryptoKeyID != 0 && !this._decrypted)
this.#decrypt()
*/
+ this.modified = true
return this._content
}//}}}
+
setContent(new_content) {//{{{
this._content = new_content
- this._modified = true
- _mbus.dispatch('NODE_MODIFIED')
/* TODO - implement crypto
if (this.CryptoKeyID == 0)
// Logic behind plaintext not being decrypted is that
@@ -533,8 +455,6 @@ export class Node {
this.data.Updated = new Date().toISOString()
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)
@@ -542,30 +462,4 @@ export class Node {
}//}}}
}
-function uuidv7() {
- // random bytes
- const value = new Uint8Array(16)
- crypto.getRandomValues(value)
-
- // current timestamp in ms
- const timestamp = BigInt(Date.now())
-
- // timestamp
- value[0] = Number((timestamp >> 40n) & 0xffn)
- value[1] = Number((timestamp >> 32n) & 0xffn)
- value[2] = Number((timestamp >> 24n) & 0xffn)
- value[3] = Number((timestamp >> 16n) & 0xffn)
- value[4] = Number((timestamp >> 8n) & 0xffn)
- value[5] = Number(timestamp & 0xffn)
-
- // version and variant
- value[6] = (value[6] & 0x0f) | 0x70
- value[8] = (value[8] & 0x3f) | 0x80
-
- const str = Array.from(value)
- .map((b) => b.toString(16).padStart(2, "0"))
- .join("")
- return `${str.slice(0, 8)}-${str.slice(8, 12)}-${str.slice(12, 16)}-${str.slice(16, 20)}-${str.slice(20)}`
-}
-
// vim: foldmethod=marker
diff --git a/static/js/node_store.mjs b/static/js/node_store.mjs
index ef43233..80aa758 100644
--- a/static/js/node_store.mjs
+++ b/static/js/node_store.mjs
@@ -159,7 +159,6 @@ export class NodeStore {
})
}//}}}
- /* TODO - Remove?
async storeNode(node) {//{{{
return new Promise((resolve, reject) => {
const t = this.db.transaction('nodes', 'readwrite')
@@ -182,7 +181,6 @@ export class NodeStore {
}
})
}//}}}
- */
async upsertNodeRecords(records) {//{{{
return new Promise((resolve, reject) => {
diff --git a/static/js/notes2.mjs b/static/js/notes2.mjs
index 7bef2ad..d73ef53 100644
--- a/static/js/notes2.mjs
+++ b/static/js/notes2.mjs
@@ -3,7 +3,6 @@ import { signal } from 'preact/signals'
import htm from 'htm'
import { Node, NodeUI } from 'node'
import { ROOT_NODE } from 'node_store'
-import { TreeNative } from 'tree'
const html = htm.bind(h)
export class Notes2 extends Component {
@@ -15,7 +14,6 @@ export class Notes2 extends Component {
startNode: null,
}
this.op = signal('')
- this.treeNative = new TreeNative()
window._sync = new Sync()
window._sync.run()
@@ -78,7 +76,6 @@ export class Notes2 extends Component {
this.nodeUI.current.setNode(node)
this.nodeUI.current.setCrumbs(ancestors)
this.tree.setSelected(node, dontExpand)
- this.treeNative.setSelected(node, dontExpand)
}//}}}
logout() {//{{{
localStorage.removeItem('session.UUID')
@@ -110,7 +107,6 @@ class Tree extends Component {
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)}>
@@ -122,7 +118,7 @@ class Tree extends Component {
`
}//}}}
componentDidMount() {//{{{
- //this.treeDiv.current.addEventListener('keydown', event => this.keyHandler(event))
+ 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
@@ -542,13 +538,13 @@ class OpSearch extends Op {
for (const r of results) {
const ancestors = r.ancestry.reverse().map(a => {
const div = tmpl(`${a.data.Name}
`)
- div[0].addEventListener('click', () => _notes2.current.goToNode(a.UUID))
+ div[0].addEventListener('click', ()=>_notes2.current.goToNode(a.UUID))
return div[0]
})
const div = tmpl(`${r.name}
`)
- div[0].addEventListener('click', () => _notes2.current.goToNode(r.uuid))
+ div[0].addEventListener('click', ()=>_notes2.current.goToNode(r.uuid))
rs.push(...div)
const ancDev = tmpl('
')
diff --git a/static/js/sync.mjs b/static/js/sync.mjs
index edc93ea..d66e27b 100644
--- a/static/js/sync.mjs
+++ b/static/js/sync.mjs
@@ -1,11 +1,31 @@
import { API } from 'api'
import { Node } from 'node'
+import { h, Component } from 'preact'
+import htm from 'htm'
+const html = htm.bind(h)
+
+const SYNC_COUNT = 1
+const SYNC_HANDLED = 2
+const SYNC_DONE = 3
export class Sync {
constructor() {//{{{
this.listeners = []
this.messagesReceived = []
}//}}}
+ addListener(fn, runMessageQueue) {//{{{
+ // Some handlers won't be added until a time after sync messages have been added to the queue.
+ // This is an opportunity for the handler to receive the old messages in order.
+ if (runMessageQueue)
+ for (const msg of this.messagesReceived)
+ fn(msg)
+ this.listeners.push(fn)
+ }//}}}
+ pushMessage(msg) {//{{{
+ this.messagesReceived.push(msg)
+ for (const fn of this.listeners)
+ fn(msg)
+ }//}}}
async run() {//{{{
try {
@@ -18,8 +38,8 @@ export class Sync {
let nodeCount = await this.getNodeCount(oldMax)
nodeCount += await nodeStore.sendQueue.count()
-
- _mbus.dispatch('SYNC_COUNT', { count: nodeCount })
+ const msg = { op: SYNC_COUNT, count: nodeCount }
+ this.pushMessage(msg)
await this.nodesFromServer(oldMax)
.then(durationNodes => {
@@ -29,7 +49,7 @@ export class Sync {
await this.nodesToServer()
} finally {
- _mbus.dispatch('SYNC_DONE')
+ this.pushMessage({ op: SYNC_DONE })
}
}//}}}
async getNodeCount(oldMax) {//{{{
@@ -40,7 +60,6 @@ export class Sync {
async nodesFromServer(oldMax) {//{{{
const syncStart = Date.now()
let syncEnd
- let handled = 0
try {
let currMax = oldMax
let offset = 0
@@ -67,14 +86,9 @@ export class Sync {
for (const i in res.Nodes) {
backendNode = new Node(res.Nodes[i], -1)
await window._sync.handleNode(backendNode)
-
- handled++
- if (handled % 100 === 0)
- _mbus.dispatch('SYNC_HANDLED', { handled })
}
} while (res.Continue)
- _mbus.dispatch('SYNC_HANDLED', { handled })
nodeStore.setAppState('latest_sync_node', currMax)
} catch (e) {
@@ -116,7 +130,7 @@ export class Sync {
} catch (e) {
console.error(e)
} finally {
- //_mbus.dispatch('SYNC_HANDLED', { count: 1 })
+ this.pushMessage({ op: SYNC_HANDLED, count: 1 })
}
}//}}}
async nodesToServer() {//{{{
@@ -135,7 +149,7 @@ export class Sync {
const res = await API.query('POST', '/sync/to_server', request)
if (!res.OK) {
// TODO - implement better error management here.
- console.error(res)
+ console.log(res)
alert(res)
return
}
@@ -143,7 +157,7 @@ export class Sync {
// Nodes are archived on server and can now be deleted from the send queue.
const keys = nodesToSend.map(node => node.ClientSequence)
await nodeStore.sendQueue.delete(keys)
- _mbus.dispatch('SYNC_UPLOADED', { count: nodesToSend.length })
+ this.pushMessage({ op: SYNC_HANDLED, count: nodesToSend.length })
} catch (e) {
console.trace(e)
@@ -154,28 +168,11 @@ export class Sync {
}//}}}
}
-export class SyncProgress {
- constructor(parentEl) {//{{{
+export class SyncProgress extends Component {
+ 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 = {
@@ -185,6 +182,9 @@ export class SyncProgress {
}
document.getElementById('sync-progress')?.classList.remove('hidden')
}//}}}
+ componentDidMount() {//{{{
+ window._sync.addListener(msg => this.progressHandler(msg), true)
+ }//}}}
getSnapshotBeforeUpdate(_, prevState) {//{{{
if (!prevState.syncedDone && this.state.syncedDone)
setTimeout(() => document.getElementById('sync-progress')?.classList.add('hidden'), 750)
@@ -202,22 +202,19 @@ export class SyncProgress {
)
}
}//}}}
- progressHandler(event) {//{{{
- const eventData = event.detail.data
- switch (event.type) {
- case 'SYNC_COUNT':
- console.log(eventData.count)
- this.state.nodesToSync = eventData.count
+ progressHandler(msg) {//{{{
+ switch (msg.op) {
+ case SYNC_COUNT:
+ this.setState({ nodesToSync: msg.count })
break
- case 'SYNC_HANDLED':
- console.log('sync handled')
- this.state.nodesSynced = eventData.handled
+ case SYNC_HANDLED:
+ this.state.nodesSynced += msg.count
break
- case 'SYNC_DONE':
+ case SYNC_DONE:
// Hides the progress bar.
- this.state.syncedDone = true
+ this.setState({ syncedDone: true })
// Don't update anything if nothing was synced.
if (this.state.nodesSynced === 0)
@@ -230,19 +227,17 @@ export class SyncProgress {
}
break
}
- 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}`
- /*
+ render(_, { nodesToSync, nodesSynced }) {//{{{
if (nodesToSync === 0)
return html`
`
- */
-
+ return html`
+
+
+
${nodesSynced} / ${nodesToSync}
+
+ `
}//}}}
}
diff --git a/static/js/tree.mjs b/static/js/tree.mjs
deleted file mode 100644
index 43649cc..0000000
--- a/static/js/tree.mjs
+++ /dev/null
@@ -1,438 +0,0 @@
-import { ROOT_NODE } from 'node_store'
-import { CustomHTMLElement } from './lib/custom_html_element.mjs'
-
-export class TreeNative {
- constructor() {// {{{
- this.treeNodeComponents = {}
- this.treeTrunk = []
- this.expandedNodes = {} // keyed on UUID
- this.selectedNode = null
- this.rendered = false
-
- this.populateFirstLevel()
- }// }}}
- render() {// {{{
- if (this.rendered)
- alert('Tree should only be rendered once.')
-
- const tmpl = document.createElement('template')
- tmpl.innerHTML = `
-
-
-
-
-
-
-
`
-
- const treeEl = tmpl.content.getElementById('tree-nodes')
-
- treeEl.addEventListener('keydown', event => this.keyHandler(event))
- tmpl.content.querySelector('.icons .search').addEventListener('click', () => _mbus.dispatch('op-search'))
- tmpl.content.querySelector('.icons .sync').addEventListener('click', () => _sync.run())
-
- tmpl.content.getElementById('logo').addEventListener('click', () => _app.goToNode(ROOT_NODE, false, false))
-
- for (const node of this.treeTrunk) {
- const treenode = new N2TreeNode(this, node)
- this.treeNodeComponents[node.UUID] = treenode
- treeEl.appendChild(treenode.render())
- }
-
- this.rendered = true
- return tmpl.content
- }// }}}
- populateFirstLevel() {//{{{
- 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)
- }
- _mbus.dispatch('TREE_TRUNK_FETCHED')
- })
- .catch(e => { console.log(e); console.log(e.type, e.error); alert(e.error) })
- }//}}}
- getNodeExpanded(UUID) {//{{{
- if (this.expandedNodes[UUID] === undefined)
- this.expandedNodes[UUID] = false
- return this.expandedNodes[UUID]
- }//}}}
- setNodeExpanded(node, value) {//{{{
- let expanded = this.expandedNodes[node.UUID]
-
- if (expanded === undefined) {
- this.expandedNodes[node.UUID] = false
- expanded = false
- }
-
- if (expanded === value)
- return
-
- this.expandedNodes[node.UUID] = value
- _mbus.dispatch(`NODE_EXPAND_${node.UUID}`, value)
- }//}}}
- setSelected(node, dontExpand) {//{{{
- if (node === undefined)
- return
-
- // 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]?.render(true)
-
- // And now the newly selected node is rerendered.
- this.treeNodeComponents[node.UUID]?.render(true)
-
- if (!dontExpand)
- this.setNodeExpanded(node, true)
- }//}}}
- isSelected(node) {//{{{
- return this.selectedNode?.UUID === node.UUID
- }//}}}
-
- 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 || n === undefined)
- return
-
- const expanded = this.getNodeExpanded(n.UUID)
- if (expanded && n.hasChildren()) {
- this.setNodeExpanded(n, false)
- return
- }
-
- if (n.isFirstSibling() && n.getParent().UUID !== ROOT_NODE) {
- _mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getParent()?.UUID, dontPush: true, dontExpand: true })
- return
- }
-
- const siblingBefore = n.getSiblingBefore()
- const siblingExpanded = this.getNodeExpanded(siblingBefore?.UUID)
- if (siblingBefore !== null && siblingExpanded && siblingBefore.hasChildren()) {
- const siblingAbove = this.getLastExpandedNode(siblingBefore)
- _mbus.dispatch("GO_TO_NODE", { nodeUUID: siblingAbove?.UUID, dontPush: true, dontExpand: true })
- return
- }
-
- _mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getSiblingBefore()?.UUID, dontPush: true, dontExpand: true })
- }//}}}
- async navigateRight(n) {//{{{
- if (n === null || n === undefined)
- return
-
- const siblingAfter = n.getSiblingAfter()
- const expanded = this.getNodeExpanded(n.UUID)
-
- if (!expanded && n.hasChildren()) {
- this.setNodeExpanded(n, true)
- return
- }
-
- if (expanded && n.hasChildren()) {
- _mbus.dispatch("GO_TO_NODE", { nodeUUID: n.Children[0]?.UUID, dontPush: true, dontExpand: true })
- return
- }
-
- if (n.isLastSibling()) {
- const nextNode = this.getParentWithNextSibling(n)
- _mbus.dispatch("GO_TO_NODE", { nodeUUID: nextNode?.UUID, dontPush: true, dontExpand: true })
- return
- }
-
- _mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getSiblingAfter()?.UUID, dontPush: true, dontExpand: true })
- }//}}}
- async navigateUp(n) {//{{{
- if (n === null || n === undefined)
- 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
- _mbus.dispatch("GO_TO_NODE", { nodeUUID: parent?.UUID, dontPush: true, dontExpand: true })
- return
- }
-
- if (siblingBefore !== null && siblingExpanded && siblingBefore.hasChildren()) {
- _mbus.dispatch("GO_TO_NODE", { nodeUUID: siblingBefore.Children[siblingBefore.Children.length - 1]?.UUID, dontPush: true, dontExpand: true })
- return
- }
-
- if (siblingBefore) {
- _mbus.dispatch("GO_TO_NODE", { nodeUUID: siblingBefore.UUID, dontPush: true, dontExpand: true })
- return
- }
- }//}}}
- async navigateDown(n) {//{{{
- if (n === null || n === undefined)
- 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)
- _mbus.dispatch("GO_TO_NODE", { nodeUUID: wantedNode?.UUID, dontPush: true, dontExpand: true })
- return
- }
-
- if (nodeExpanded && n.isLastSibling() && !n.hasChildren()) {
- const wantedNode = this.getParentWithNextSibling(n)
- _mbus.dispatch("GO_TO_NODE", { nodeUUID: wantedNode?.UUID, dontPush: true, dontExpand: true })
- return
- }
-
- // Node not expanded. Go to this node's next sibling.
- // GoToNode will abort if given null.
- if (!nodeExpanded || !n.hasChildren()) {
- _mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getSiblingAfter()?.UUID, dontPush: true, dontExpand: true })
- return
- }
-
- // Node is expanded.
- // Children will be visually beneath this node, if any.
- if (nodeExpanded && n.hasChildren()) {
- _mbus.dispatch("GO_TO_NODE", { nodeUUID: n.Children[0].UUID, dontPush: true, dontExpand: true })
- return
- }
- }//}}}
- async navigateTop() {//{{{
- const root = await nodeStore.get(ROOT_NODE)
- if (root.Children.length === 0)
- return
- _mbus.dispatch("GO_TO_NODE", { nodeUUID: root.Children[0]?.UUID, dontPush: true, dontExpand: 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)
- _mbus.dispatch("GO_TO_NODE", { nodeUUID: lastnode?.UUID, dontPush: true, dontExpand: true })
- } else
- _mbus.dispatch("GO_TO_NODE", { nodeUUID: root.Children[root.Children.length - 1]?.UUID, dontPush: true, dontExpand: true })
- }//}}}
-
- 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 makeVisible(node) {// {{{
- const treenode = this.treeNodeComponents[node.UUID]
-
- const ancestors = await nodeStore.getNodeAncestry(node)
- for (const ancestor of ancestors.reverse()) {
- this.setNodeExpanded(ancestor, true)
- }
-
- // The ROOT_NODE for example hasn't got a treenode.
- treenode?.scrollIntoView({ block: 'nearest' })
- }// }}}
-}
-
-export class N2TreeNode extends CustomHTMLElement {
- static {// {{{
- this.tmpl = document.createElement('template')
- this.tmpl.innerHTML = `
-
-
-
-
-
- `
- }// }}}
-
- constructor(tree, node, parent) {//{{{
- super()
- this.classList.add('node')
-
- this.tree = tree
- this.node = node
- this.parent = parent
-
- this.children_populated = false
- this.rendered = false
-
- this.elExpandToggle.addEventListener('click', () => this.tree.setNodeExpanded(this.node, !this.tree.getNodeExpanded(this.node.UUID)))
- this.elName.addEventListener('click', () => _mbus.dispatch('TREE_NODE_SELECTED', this.node))
-
- _mbus.subscribe(`NODE_CHILDREN_FETCHED_${node.UUID}`, () => {
- this.render(true)
- })
-
- _mbus.subscribe(`NODE_EXPAND_${node.UUID}`, state => {
- this.render(true)
- })
-
- if (this.node.Level === 0 || this.tree.getNodeExpanded(this.node.UUID))
- this.fetchChildren()
- }// }}}
- async fetchChildren() {//{{{
- await this.node.fetchChildren()
- this.children_populated = true
- }//}}}
- render(force_update) {//{{{
- if (this.rendered && force_update !== true)
- return this
-
- // Fetch the next level of children if the parent tree node is expanded and our children thus will be visible.
- const expanded = this.node.Children.length > 0 && this.tree.getNodeExpanded(this.node.UUID)
-
- if (!this.children_populated && this.tree.getNodeExpanded(this.parent?.node.UUID)) {
- this.node.fetchChildren().then(() => this.children_populated = true)
- }
-
- // Update the name and selected status
- this.elName.innerText = this.node.get('Name')
- if (this.tree.isSelected(this.node))
- this.elName.classList.add('selected')
- else
- this.elName.classList.remove('selected')
-
- // Update expansion state
- if (expanded) {
- this.elChildren.classList.add('expanded')
- this.elChildren.classList.remove('collapsed')
- } else {
- this.elChildren.classList.remove('expanded')
- this.elChildren.classList.add('collapsed')
- }
-
- // The expand icon
is only changed to not get a flickering when re-rendering.
- if (this.node.Children.length === 0)
- this.setImgSrc(this.elExpand, `/images/${window._VERSION}/leaf.svg`)
- else if (this.tree.getNodeExpanded(this.node.UUID))
- this.setImgSrc(this.elExpand, `/images/${window._VERSION}/expanded.svg`)
- else
- this.setImgSrc(this.elExpand, `/images/${window._VERSION}/collapsed.svg`)
-
- // Should children be rendered?
- this.elChildren.innerHTML = ''
- let children = []
- if (expanded)
- children = this.node.Children.map(node => {
- let treenode = this.tree.treeNodeComponents[node.UUID]
- if (treenode === undefined) {
- treenode = new N2TreeNode(this.tree, node, this)
- this.tree.treeNodeComponents[node.UUID] = treenode
- }
- return treenode
- })
-
- for (const c of children)
- this.elChildren.appendChild(c.render())
-
- this.rendered = true
- return this
- }//}}}
-
- setImgSrc(img, newSrc) {// {{{
- if (img.getAttribute('src') === newSrc)
- return
- img.setAttribute('src', newSrc)
- }// }}}
-}
-customElements.define('n2-treenode', N2TreeNode)
-
-// vim: foldmethod=marker
diff --git a/static/less/notes2.less b/static/less/notes2.less
index 2c57f53..a1ae783 100644
--- a/static/less/notes2.less
+++ b/static/less/notes2.less
@@ -48,8 +48,8 @@ html {
#tree {
grid-area: tree;
- display: grid;
- padding: 16px 0px 16px 16px;
+ padding: 16px 32px;
+ background-color: #333;
color: #ddd;
z-index: 100; // Over crumbs shadow
border-left: 2px solid #333;
@@ -62,12 +62,9 @@ html {
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;
@@ -127,19 +124,12 @@ html {
}
}
-#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;
+ margin: 0px 16px;
.crumbs {
background: #e4e4e4;
@@ -164,11 +154,6 @@ html {
cursor: pointer;
user-select: none;
-webkit-tap-highlight-color: transparent;
-
- a {
- text-decoration: none;
- color: inherit;
- }
}
.crumb:after {
diff --git a/static/less/theme.less b/static/less/theme.less
index b9c47ed..255a49c 100644
--- a/static/less/theme.less
+++ b/static/less/theme.less
@@ -1,5 +1,3 @@
-:root {
- --color1: #fe5f55;
- --color2: #efede8;
- --color3: #666;
-}
+@color1: #fe5f55;
+@color2: #efede8;
+@color3: #666;
diff --git a/views/layouts/main.gotmpl b/views/layouts/main.gotmpl
index 470cfe5..123fc68 100644
--- a/views/layouts/main.gotmpl
+++ b/views/layouts/main.gotmpl
@@ -4,6 +4,16 @@
+
+
-
-
diff --git a/views/pages/notes2.gotmpl b/views/pages/notes2.gotmpl
index 1a82a90..f633692 100644
--- a/views/pages/notes2.gotmpl
+++ b/views/pages/notes2.gotmpl
@@ -1,16 +1,5 @@
{{ define "page" }}
-
-
+