diff --git a/main.go b/main.go
index a9ac728..96f08aa 100644
--- a/main.go
+++ b/main.go
@@ -25,7 +25,7 @@ import (
const VERSION = "v1"
const CONTEXT_USER = 1
-const SYNC_PAGINATION = 100
+const SYNC_PAGINATION = 200
var (
FlagGenerate bool
@@ -269,9 +269,11 @@ 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
@@ -288,7 +290,6 @@ 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)
@@ -334,9 +335,14 @@ func actionSyncToServer(w http.ResponseWriter, r *http.Request) { // {{{
return
}
- db.Exec(`CALL add_nodes($1, $2, $3::jsonb)`, user.UserID, user.ClientUUID, request.NodeData)
+ _, 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
+ }
- responseData(w, map[string]interface{}{
+ responseData(w, map[string]any{
"OK": true,
})
} // }}}
diff --git a/sql/00006.sql b/sql/00006.sql
index 6b0ea9b..453b260 100644
--- a/sql/00006.sql
+++ b/sql/00006.sql
@@ -1,16 +1 @@
-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);
+DROP INDEX public.node_uuid_idx;
diff --git a/static/css/login.css b/static/css/login.css
index 88a9140..7e19cb8 100644
--- a/static/css/login.css
+++ b/static/css/login.css
@@ -1,37 +1,44 @@
+@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 #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;
+ 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;
+ }
}
diff --git a/static/css/main.css b/static/css/main.css
index 75f1925..a8924d9 100644
--- a/static/css/main.css
+++ b/static/css/main.css
@@ -1,23 +1,29 @@
+@import "theme.css";
+
html {
- box-sizing: border-box;
- background: #efede8;
- font-family: "Liberation Mono", monospace;
- font-size: 14px;
- margin: 0px;
- padding: 0px;
+ box-sizing: border-box;
+ background: var(--color2);
+ 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 b6b0963..e6d0ef6 100644
--- a/static/css/notes2.css
+++ b/static/css/notes2.css
@@ -1,264 +1,361 @@
+@import "theme.css";
+
+:root {
+ --content-width: 900px;
+}
+
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 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;
- }
+ min-height: 100vh;
+
+ display: grid;
+ grid-template-areas:
+ "tree crumbs"
+ "tree name"
+ "tree sync"
+ "tree content"
+ /*
+ "tree checklist"
+ "tree files"
+ */
+ "tree blank"
+ ;
+ grid-template-columns: min-content 1fr;
+ grid-template-rows:
+ min-content min-content 48px 1fr;
+
+
+ @media only screen and (max-width: 600px) {
+ grid-template-areas:
+ "crumbs"
+ "sync"
+ "name"
+ "content"
+ /*
+ "checklist"
+ "files"
+ */
+ "blank"
+ ;
+ grid-template-columns: 1fr;
+
+ #tree {
+ display: none;
+ }
+
+ n2-syncprogress {
+ .el-count {
+ top: 4px;
+ }
+ }
+ }
+
}
+
#tree {
- grid-area: tree;
- padding: 16px 32px;
- background-color: #333;
- color: #ddd;
- z-index: 100;
- border-left: 2px solid #333;
+ 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;
+ }
+ }
+ }
}
-#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;
+
+#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: 0px 16px;
+ grid-area: crumbs;
+ display: grid;
+ align-items: start;
+ justify-items: center;
+ height: min-content;
+ margin: 16px 16px;
+
+ n2-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);
+ }
+ }
+
+ n2-crumb {
+ margin-right: 8px;
+ cursor: pointer;
+ user-select: none;
+ -webkit-tap-highlight-color: transparent;
+
+ a {
+ text-decoration: none;
+ color: inherit;
+ }
+ }
+
+ n2-crumb:after {
+ content: ">";
+ font-weight: bold;
+ color: var(--color1)
+ }
+
+ n2-crumb:last-child {
+ margin-right: 0;
+ }
+
+ n2-crumb:last-child:after {
+ content: '';
+ margin-left: 0px;
+ }
+
+ }
+
}
-#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 {
- 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;
-}
-/* ============================================================= *
- * 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: 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;
-}
-.grow-wrap > textarea {
- /* You could leave this, but after a user resizes, then it ruins the auto sizing */
- resize: none;
- /* Firefox shows scrollbar on growth, you can hide like this. */
- overflow: hidden;
-}
-.grow-wrap > textarea,
-.grow-wrap::after {
- /* Identical styling required!! */
- padding: 0.5rem;
- font: inherit;
- /* Place on top of each other */
- grid-area: 1 / 1 / 2 / 2;
+
+n2-syncprogress {
+ --radius: 8px;
+
+ display: grid;
+ grid-area: sync;
+ display: grid;
+ justify-items: center;
+ align-items: center;
+
+ position: relative;
+
+ opacity: 0;
+ transition: height 0s 500ms, opacity 500ms linear, visibility 0s 500ms;
+
+ &.show {
+ opacity: 1;
+ transition: visibility, height 0s, opacity 500ms linear;
+ }
+
+ progress {
+ width: calc(100% - 32px);
+ max-width: var(--content-width);
+ height: 24px;
+ border-radius: 8px;
+ }
+
+ .count {
+ position: absolute;
+ top: 16px;
+ width: 100%;
+ white-space: nowrap;
+ color: #888;
+ text-align: center;
+ font-size: 12pt;
+ font-weight: bold;
+ }
+
+ 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);
+ }
+
}
+
/* ============================================================= */
-#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;
-}
-#node-content:invalid {
- background: #f5f5f5;
- padding-top: 16px;
+
+n2-nodeui {
+ margin-bottom: 32px;
+
+ .el-name {
+ color: #333;
+ font-weight: bold;
+ text-align: center;
+ font-size: 1.15em;
+ margin-top: 8px;
+ margin-bottom: 0px;
+ }
+
+ .el-node-content {
+ justify-self: center;
+ word-wrap: break-word;
+ font-family: monospace;
+ color: #333;
+
+ /*
+ width: 100%;
+ max-width: var(--content-width);
+ field-sizing: content;
+ */
+
+ width: calc(100% - 32px);
+ max-width: var(--content-width);
+ field-sizing: content;
+
+ resize: none;
+ border: none;
+ outline: none;
+
+ padding: 16px 0;
+ border-top: 1px solid #e0e0e0;
+ border-bottom: 1px solid #e0e0e0;
+ margin-bottom: 32px;
+
+ &:invalid {
+ background: #f5f5f5;
+ padding-top: 16px;
+ }
+ }
}
+
#blank {
- grid-area: blank;
- height: 32px;
+ grid-area: blank;
+ height: 32px;
}
-dialog.op::backdrop {
- background: rgba(0, 0, 0, 0.5);
+
+dialog.op {
+ &::backdrop {
+ background: rgba(0, 0, 0, 0.5);
+ }
+
+ .header {
+ font-weight: bold;
+ margin-top: 16px;
+
+ &:first-child {
+ margin-top: 0px;
+ }
+ }
+
}
-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: "";
+
+#op-search {
+ .results {
+ display: grid;
+ grid-template-columns: min-content min-content;
+ grid-gap: 6px 16px;
+
+ div {
+ white-space: nowrap;
+ }
+
+
+ .ancestors {
+ display: flex;
+
+ .ancestor::after {
+ content: ">";
+ margin: 0px 8px;
+ color: #a00;
+ }
+
+ .ancestor:last-child::after {
+ content: "";
+ }
+ }
+ }
}
diff --git a/static/css/theme.css b/static/css/theme.css
index e69de29..b9c47ed 100644
--- a/static/css/theme.css
+++ b/static/css/theme.css
@@ -0,0 +1,5 @@
+:root {
+ --color1: #fe5f55;
+ --color2: #efede8;
+ --color3: #666;
+}
diff --git a/static/js/app.mjs b/static/js/app.mjs
new file mode 100644
index 0000000..45cd85b
--- /dev/null
+++ b/static/js/app.mjs
@@ -0,0 +1,329 @@
+import { ROOT_NODE } from 'node_store'
+import { CustomHTMLElement } from './lib/custom_html_element.mjs'
+import { N2Tree } from 'tree'
+import { Node } from 'node'
+
+export class App {
+ constructor() {// {{{
+ this.currentNode = null
+ this.tree = new N2Tree()
+ this.crumbs = new N2Crumbs()
+ this.crumbsElement = document.getElementById('crumbs')
+ this.nodeUI = document.getElementById('note')
+
+ _mbus.subscribe('TREE_TRUNK_FETCHED', async () => {
+ document.getElementById('tree').append(this.tree.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))
+ window.addEventListener('popstate', event => this.popState(event))
+ document.getElementById('notes2').addEventListener('click', event => {
+ if (event.target.id === 'notes2')
+ document.getElementById('node-content')?.focus()
+ })
+
+ window._sync = new Sync()
+
+ // I think it is uncomfortable having the sync running as soon as the page load.
+ // I haven't gotten the time to look at the page before stuff jumps around.
+ // There a slight delay to initiate sync seems reasonable.
+ setTimeout(() => window._sync.run(), 1000)
+ }// }}}
+
+ keyHandler(event) {//{{{
+ 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')
+ this.nodeUI.takeFocus()
+ else
+ this.nodeUI.takeFocus()
+ 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()
+ }
+ }//}}}
+ popState(event) {// {{{
+ _mbus.dispatch("GO_TO_NODE", { nodeUUID: event.state.nodeUUID, dontPush: true, dontExpand: true })
+ }// }}}
+ 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.tree.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.tree.makeVisible(node)
+ }//}}}
+}
+
+class N2Crumbs extends CustomHTMLElement {
+ static {// {{{
+ this.tmpl = document.createElement('template')
+ this.tmpl.innerHTML = `
+ `
+ }// }}}
+ constructor() {// {{{
+ super()
+ this.classList.add('crumbs')
+
+ this.crumbs = []
+
+ _mbus.subscribe('CRUMBS_SET', event => {
+ this.crumbs = event.detail.data
+ })
+ }// }}}
+ render() {// {{{
+ const crumbs = this.crumbs.map(node =>
+ new N2Crumb(
+ node.get('Name'),
+ node.UUID,
+ )
+ )
+
+ const start = new N2Crumb('Start', ROOT_NODE)
+ crumbs.push(start)
+
+ this.replaceChildren(...crumbs.reverse())
+ return this
+ }// }}}
+}
+customElements.define('n2-crumbs', N2Crumbs)
+
+class N2Crumb extends CustomHTMLElement {
+ static {// {{{
+ this.tmpl = document.createElement('template')
+ this.tmpl.innerHTML = `
+
+ `
+ }// }}}
+ constructor(label, uuid) {// {{{
+ super()
+ this.classList.add('crumb')
+
+ this.label = label
+ this.uuid = uuid
+
+ this.elLink.href = `/notes2#${this.uuid}`
+ this.elLink.innerText = this.label
+ 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')
+ 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(`
+
+
-
-
- ${page}
- `
- }//}}}
- async componentDidMount() {//{{{
- _notes2.current.goToNode(this.props.startNode.UUID, true)
- _notes2.current.tree.expandToTrunk(this.props.startNode)
- }//}}}
- setNode(node) {//{{{
- this.nodeModified.value = false
- this.node.value = node
- }//}}}
- setCrumbs(nodes) {//{{{
- this.crumbs = nodes
- }//}}}
- async saveNode() {//{{{
- if (!this.nodeModified.value)
- return
-
- /* The node history is a local store for node history.
- * This could be provisioned from the server or cleared if
- * deemed unnecessary.
- *
- * The send queue is what will be sent back to the server
- * to have a recorded history of the notes.
- *
- * A setting to be implemented in the future could be to
- * not save the history locally at all. */
- const node = this.node.value
-
- // The node is still in its old state and will present
- // the unmodified content to the node store.
- const history = nodeStore.nodesHistory.add(node)
-
- // Prepares the node object for saving.
- // Sets Updated value to current date and time.
- await node.save()
-
- // Updated node is added to the send queue to be stored on server.
- const sendQueue = nodeStore.sendQueue.add(this.node.value)
-
- // Updated node is saved to the primary node store.
- const nodeStoreAdding = nodeStore.add([node])
-
- await Promise.all([history, sendQueue, nodeStoreAdding])
- this.nodeModified.value = false
- }//}}}
-
- keyHandler(evt) {//{{{
- let handled = true
-
- // All keybindings is Alt+Shift, since the popular browsers at the time (2023) allows to override thees.
- // Ctrl+S is the exception to using Alt+Shift, since it is overridable and in such widespread use for saving.
- // Thus, the exception is acceptable to consequent use of alt+shift.
- if (!(evt.shiftKey && evt.altKey) && !(evt.key.toUpperCase() === 'S' && evt.ctrlKey))
- return
-
- switch (evt.key.toUpperCase()) {
- case 'T':
- if (document.activeElement.id === 'tree')
- document.getElementById('node-content').focus()
- else
- document.getElementById('tree').focus()
- break
-
- case 'F':
- _mbus.dispatch('op-search')
- break
- /*
- case 'C':
- this.showPage('node')
- break
-
- case 'E':
- this.showPage('keys')
- break
-
- case 'M':
- this.toggleMarkdown()
- break
-
- case 'N':
- this.createNode()
- break
-
- case 'P':
- this.showPage('node-properties')
- break
-
- */
- case 'S':
- if (this.page.value === 'node')
- this.saveNode()
- else if (this.page.value === 'node-properties')
- this.nodeProperties.current.save()
- break
- /*
-
- case 'U':
- this.showPage('upload')
- break
-
- case 'F':
- this.showPage('search')
- break
- */
-
- default:
- handled = false
- }
-
- if (handled) {
- evt.preventDefault()
- evt.stopPropagation()
- }
- }//}}}
-}
-
-class NodeContent extends Component {
- constructor(props) {//{{{
- super(props)
- this.contentDiv = createRef()
- this.state = {
- modified: false,
- }
- }//}}}
- render({ node }) {//{{{
- let content = ''
- try {
- content = node.content()
- } catch (err) {
- return html`
-
${err.message}
- `
- }
-
- /*
- let element
- if (node.RenderMarkdown.value)
- element = html`<${MarkdownContent} key='markdown-content' content=${content} />`
- else
- */
- const element = html`
-
-
-
- `
-
- return element
- }//}}}
- componentDidMount() {//{{{
- this.resize()
- window.addEventListener('resize', () => this.resize())
-
- const contentResizeObserver = new ResizeObserver(entries => {
- for (const idx in entries) {
- const w = entries[idx].contentRect.width
- document.querySelector('#crumbs .crumbs').style.width = `${w}px`
- }
- });
-
- const nodeContent = document.getElementById('node-content')
- contentResizeObserver.observe(nodeContent);
-
- }//}}}
- componentDidUpdate() {//{{{
- this.resize()
- }//}}}
- contentChanged(evt) {//{{{
- _notes2.current.nodeUI.current.nodeModified.value = true
- this.props.node.setContent(evt.target.value)
- this.resize()
- }//}}}
- resize() {//{{{
- const textarea = document.getElementById('node-content')
- if (textarea)
- textarea.parentNode.dataset.replicatedValue = textarea.value
+ _mbus.subscribe('NODE_MODIFIED', () => {
+ document.querySelector('#crumbs .crumbs')?.classList.add('node-modified')
+ })
+
+ _mbus.subscribe('NODE_UNMODIFIED', () => {
+ document.querySelector('#crumbs .crumbs')?.classList.remove('node-modified')
+ })
+
+ this.elNodeContent.addEventListener('input', event => this.contentChanged(event))
+ }// }}}
+ render() {// {{{
+ this.elName.innerText = this.node?.get('Name') ?? ''
+ this.elNodeContent.value = this.node?.get('Content') ?? ''
+ }// }}}
+ takeFocus() {// {{{
+ this.elNodeContent.focus()
+ }// }}}
+
+ contentChanged(event) {//{{{
+ this.node.setContent(event.target.value)
}//}}}
+ isModified() {// {{{
+ return this.node?.isModified()
+ }// }}}
}
+customElements.define('n2-nodeui', N2NodeUI)
export class Node {
static sort(a, b) {//{{{
@@ -337,10 +54,22 @@ export class Node {
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.
@@ -354,13 +83,12 @@ 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
@@ -377,6 +105,10 @@ export class Node {
*/
}//}}}
+ reset() {// {{{
+ this._content = this.data.Content
+ this._modified = false
+ }// }}}
get(prop) {//{{{
return this.data[prop]
}//}}}
@@ -384,13 +116,13 @@ 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
}//}}}
async fetchChildren() {//{{{
- if (this._children_fetched)
- return this.Children
-
this.Children = await nodeStore.getTreeNodes(this.UUID, this.Level + 1)
this._children_fetched = true
@@ -408,7 +140,8 @@ export class Node {
}
// Notify the tree that all children are fetched and ready to process.
- _notes2.current.tree.fetchChildrenOn(this.UUID)
+ //_notes2.current.tree.fetchChildrenOn(this.UUID)
+ _mbus.dispatch(`NODE_CHILDREN_FETCHED_${this.UUID}`)
return this.Children
}//}}}
@@ -435,12 +168,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
@@ -455,6 +188,8 @@ 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)
@@ -462,4 +197,30 @@ 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 80aa758..e849e29 100644
--- a/static/js/node_store.mjs
+++ b/static/js/node_store.mjs
@@ -13,7 +13,7 @@ export class NodeStore {
this.sendQueue = null
this.nodesHistory = null
}//}}}
- async initializeDB() {//{{{
+ initializeDB() {//{{{
return new Promise((resolve, reject) => {
const req = indexedDB.open('notes', 7)
@@ -78,7 +78,7 @@ export class NodeStore {
}
})
}//}}}
- async initializeRootNode() {//{{{
+ initializeRootNode() {//{{{
return new Promise((resolve, reject) => {
// The root node is a magical node which displays as the first node if none is specified.
// If not already existing, it will be created.
@@ -120,7 +120,7 @@ export class NodeStore {
return n
}//}}}
- async getAppState(key) {//{{{
+ getAppState(key) {//{{{
return new Promise((resolve, reject) => {
const trx = this.db.transaction('app_state', 'readonly')
const appState = trx.objectStore('app_state')
@@ -135,7 +135,7 @@ export class NodeStore {
getRequest.onerror = (event) => reject(event.target.error)
})
}//}}}
- async setAppState(key, value) {//{{{
+ setAppState(key, value) {//{{{
return new Promise((resolve, reject) => {
try {
const t = this.db.transaction('app_state', 'readwrite')
@@ -159,30 +159,8 @@ export class NodeStore {
})
}//}}}
- async storeNode(node) {//{{{
- return new Promise((resolve, reject) => {
- const t = this.db.transaction('nodes', 'readwrite')
- const nodeStore = t.objectStore('nodes')
- t.onerror = (event) => {
- console.log('transaction error', event.target.error)
- reject(event.target.error)
- }
- t.oncomplete = () => {
- resolve()
- }
-
- const nodeReq = nodeStore.put(node.data)
- nodeReq.onsuccess = () => {
- console.debug(`Storing ${node.UUID} (${node.get('Name')})`)
- }
- queueReq.onerror = (event) => {
- console.log(`Error storing ${node.UUID}`, event.target.error)
- reject(event.target.error)
- }
- })
- }//}}}
-
- async upsertNodeRecords(records) {//{{{
+ /*
+ upsertNodeRecords(records) {//{{{
return new Promise((resolve, reject) => {
const t = this.db.transaction('nodes', 'readwrite')
const nodeStore = t.objectStore('nodes')
@@ -210,17 +188,11 @@ export class NodeStore {
record.modified = 0
addReq = nodeStore.put(record)
}
- addReq.onsuccess = () => {
- console.debug(`${op} ${record.UUID} (${record.Name})`)
- }
- addReq.onerror = (event) => {
- console.log(`error ${op} ${record.UUID}`, event.target.error)
- reject(event.target.error)
- }
}
})
}//}}}
- async getTreeNodes(parent, newLevel) {//{{{
+ */
+ getTreeNodes(parent, newLevel) {//{{{
return new Promise((resolve, reject) => {
// Parent of toplevel nodes is ROOT_NODE in indexedDB.
// Only the root node has '' as parent.
@@ -242,7 +214,7 @@ export class NodeStore {
req.onerror = (event) => reject(event.target.error)
})
}//}}}
- async search(searchfor, parent) {//{{{
+ search(searchfor, parent) {//{{{
return new Promise((resolve, reject) => {
const trx = this.db.transaction('nodes', 'readonly')
const nodeStore = trx.objectStore('nodes')
@@ -272,46 +244,55 @@ export class NodeStore {
})
}//}}}
- async add(records) {//{{{
+ add(records, objstore) {//{{{
return new Promise((resolve, reject) => {
try {
- const t = this.db.transaction('nodes', 'readwrite')
- const nodeStore = t.objectStore('nodes')
- t.onerror = (event) => {
- console.log('transaction error', event.target.error)
- reject(event.target.error)
+ // A nodestore can be provided in order to
+ // avoid creating new transactions.
+ let nodeStore = objstore
+ let t
+
+ if (nodeStore === undefined) {
+ t = this.db.transaction('nodes', 'readwrite')
+ nodeStore = t.objectStore('nodes')
+
+ t.oncomplete = (_event) => {
+ resolve()
+ }
+
+ t.onerror = (event) => {
+ console.error('transaction error', event.target.error)
+ reject(event.target.error)
+ }
}
// records is an object, not an array.
- const promises = []
for (const recordIdx in records) {
const record = records[recordIdx]
- const addReq = nodeStore.put(record.data)
-
- const promise = new Promise((resolve, reject) => {
- addReq.onsuccess = () => {
- console.debug('OK!', record.ID, record.Name)
- resolve()
- }
- addReq.onerror = (event) => {
- console.log('Error!', event.target.error, record.ID)
- reject(event.target.error)
- }
- })
- promises.push(promise)
+ nodeStore.put(record.data)
}
- Promise.all(promises).then(() => resolve())
+ resolve()
} catch (e) {
- console.log(e)
+ console.error(e)
+ reject(e)
}
})
}//}}}
- async get(uuid) {//{{{
+ get(uuid, suppliedNodestore) {//{{{
return new Promise((resolve, reject) => {
- const trx = this.db.transaction('nodes', 'readonly')
- const nodeStore = trx.objectStore('nodes')
+ // A nodestore can be provided in order to
+ // avoid creating new transactions.
+ let trx
+ let nodeStore = suppliedNodestore
+
+ if (nodeStore === undefined) {
+ trx = this.db.transaction('nodes', 'readonly')
+ nodeStore = trx.objectStore('nodes')
+ }
+
const getRequest = nodeStore.get(uuid)
+
getRequest.onsuccess = (event) => {
// Node not found in IndexedDB.
if (event.target.result === undefined) {
@@ -323,7 +304,7 @@ export class NodeStore {
}
})
}//}}}
- async getNodeAncestry(node, accumulated) {//{{{
+ getNodeAncestry(node, accumulated) {//{{{
return new Promise((resolve, reject) => {
if (accumulated === undefined)
accumulated = []
@@ -354,8 +335,11 @@ export class NodeStore {
})
}//}}}
+ newTransaction(objectStore, mode) {// {{{
+ return this.db.transaction(objectStore, mode)
+ }// }}}
- async nodeCount() {//{{{
+ nodeCount() {//{{{
return new Promise((resolve, reject) => {
const t = this.db.transaction('nodes', 'readwrite')
const nodeStore = t.objectStore('nodes')
@@ -372,7 +356,7 @@ class SimpleNodeStore {
this.db = db
this.storeName = storeName
}//}}}
- async add(node) {//{{{
+ add(node) {//{{{
return new Promise((resolve, reject) => {
const t = this.db.transaction(['nodes', this.storeName], 'readwrite')
const store = t.objectStore(this.storeName)
@@ -392,7 +376,7 @@ class SimpleNodeStore {
}
})
}//}}}
- async retrieve(limit) {//{{{
+ retrieve(limit) {//{{{
return new Promise((resolve, reject) => {
const cursorReq = this.db
.transaction(['nodes', this.storeName], 'readonly')
@@ -420,7 +404,7 @@ class SimpleNodeStore {
}
})
}//}}}
- async delete(keys) {//{{{
+ delete(keys) {//{{{
const store = this.db
.transaction(['nodes', this.storeName], 'readwrite')
.objectStore(this.storeName)
@@ -437,7 +421,7 @@ class SimpleNodeStore {
}
return Promise.all(promises)
}//}}}
- async count() {//{{{
+ count() {//{{{
const store = this.db
.transaction(['nodes', this.storeName], 'readonly')
.objectStore(this.storeName)
diff --git a/static/js/notes2.mjs b/static/js/notes2.mjs
index d73ef53..7bef2ad 100644
--- a/static/js/notes2.mjs
+++ b/static/js/notes2.mjs
@@ -3,6 +3,7 @@ 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 {
@@ -14,6 +15,7 @@ export class Notes2 extends Component {
startNode: null,
}
this.op = signal('')
+ this.treeNative = new TreeNative()
window._sync = new Sync()
window._sync.run()
@@ -76,6 +78,7 @@ 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')
@@ -107,6 +110,7 @@ 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)}>
@@ -118,7 +122,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
@@ -538,13 +542,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 d66e27b..9b58cf7 100644
--- a/static/js/sync.mjs
+++ b/static/js/sync.mjs
@@ -1,31 +1,12 @@
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
+import { CustomHTMLElement } from './lib/custom_html_element.mjs'
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 {
@@ -38,8 +19,8 @@ export class Sync {
let nodeCount = await this.getNodeCount(oldMax)
nodeCount += await nodeStore.sendQueue.count()
- const msg = { op: SYNC_COUNT, count: nodeCount }
- this.pushMessage(msg)
+
+ _mbus.dispatch('SYNC_COUNT', { count: nodeCount })
await this.nodesFromServer(oldMax)
.then(durationNodes => {
@@ -49,7 +30,7 @@ export class Sync {
await this.nodesToServer()
} finally {
- this.pushMessage({ op: SYNC_DONE })
+ _mbus.dispatch('SYNC_DONE')
}
}//}}}
async getNodeCount(oldMax) {//{{{
@@ -60,6 +41,7 @@ export class Sync {
async nodesFromServer(oldMax) {//{{{
const syncStart = Date.now()
let syncEnd
+ let handled = 0
try {
let currMax = oldMax
let offset = 0
@@ -83,12 +65,24 @@ export class Sync {
* sync be preserved in the backend. */
let backendNode = null
+
+ // Create a single transaction to be used in the chain of
+ // this sync. Otherwise it would take more time to create
+ // transactions for each node.
+ const trx = nodeStore.newTransaction('nodes', 'readwrite')
+ const objstore = trx.objectStore('nodes')
+
for (const i in res.Nodes) {
backendNode = new Node(res.Nodes[i], -1)
- await window._sync.handleNode(backendNode)
+ await this.handleNode(backendNode, objstore)
+
+ 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) {
@@ -101,16 +95,16 @@ export class Sync {
}
return (syncEnd - syncStart)
}//}}}
- async handleNode(backendNode) {//{{{
+ async handleNode(backendNode, objstore) {//{{{
try {
/* Retrieving the local copy of this node from IndexedDB.
* The backend node can be discarded if it is older than
* the local copy since it is considered history preserved
* in the backend. */
- return nodeStore.get(backendNode.UUID)
- .then(async localNode => {
+ return nodeStore.get(backendNode.UUID, objstore)
+ .then(localNode => {
if (localNode.updated() >= backendNode.updated()) {
- console.log(`History from backend: ${backendNode.UUID}`)
+ console.debug(`History from backend: ${backendNode.UUID}`)
return
}
@@ -120,17 +114,17 @@ export class Sync {
*
* If the local node has seen change, the change is already
* placed into the send_queue anyway. */
- return nodeStore.add([backendNode])
+ return nodeStore.add([backendNode], objstore)
})
- .catch(async () => {
+ .catch(() => {
// Not found in IndexedDB - OK to just insert since it only exists in backend.
- return nodeStore.add([backendNode])
+ return nodeStore.add([backendNode], objstore)
})
} catch (e) {
console.error(e)
} finally {
- this.pushMessage({ op: SYNC_HANDLED, count: 1 })
+ //_mbus.dispatch('SYNC_HANDLED', { count: 1 })
}
}//}}}
async nodesToServer() {//{{{
@@ -149,7 +143,7 @@ export class Sync {
const res = await API.query('POST', '/sync/to_server', request)
if (!res.OK) {
// TODO - implement better error management here.
- console.log(res)
+ console.error(res)
alert(res)
return
}
@@ -157,7 +151,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)
- this.pushMessage({ op: SYNC_HANDLED, count: nodesToSend.length })
+ _mbus.dispatch('SYNC_UPLOADED', { count: nodesToSend.length })
} catch (e) {
console.trace(e)
@@ -168,77 +162,66 @@ export class Sync {
}//}}}
}
-export class SyncProgress extends Component {
+export class N2SyncProgress extends CustomHTMLElement {
+ static {
+ this.tmpl = document.createElement('template')
+ this.tmpl.innerHTML = `
+
+
0 / 0
+ `
+ }
constructor() {//{{{
super()
+
this.reset()
+ _mbus.subscribe('SYNC_COUNT', event => this.progressHandler(event))
+ _mbus.subscribe('SYNC_HANDLED', event => this.progressHandler(event))
+ _mbus.subscribe('SYNC_DONE', event => this.progressHandler(event))
}//}}}
reset() {//{{{
- this.forceUpdateRequest = null
this.state = {
nodesToSync: 0,
nodesSynced: 0,
- syncedDone: false,
- }
- 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)
- }//}}}
- componentDidUpdate() {//{{{
- if (!this.state.syncedDone) {
- if (this.forceUpdateRequest !== null)
- clearTimeout(this.forceUpdateRequest)
- this.forceUpdateRequest = setTimeout(
- () => {
- this.forceUpdateRequest = null
- this.forceUpdate()
- },
- 50
- )
}
}//}}}
- progressHandler(msg) {//{{{
- switch (msg.op) {
- case SYNC_COUNT:
- this.setState({ nodesToSync: msg.count })
+ progressHandler(event) {//{{{
+ const eventData = event.detail.data
+ switch (event.type) {
+ case 'SYNC_COUNT':
+ this.state.nodesToSync = eventData.count
+ this.setSyncState(true)
break
- case SYNC_HANDLED:
- this.state.nodesSynced += msg.count
+ case 'SYNC_HANDLED':
+ this.state.nodesSynced = eventData.handled
break
- case SYNC_DONE:
+ case 'SYNC_DONE':
// Hides the progress bar.
- this.setState({ syncedDone: true })
+ 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.
- if (window._notes2?.current?.reloadTree.value !== null) {
- nodeStore.purgeCache()
- window._notes2.current.reloadTree.value = window._notes2.current.reloadTree.value + 1
- }
+ window._app.tree.reset()
break
}
+ this.render()
}//}}}
- render(_, { nodesToSync, nodesSynced }) {//{{{
- if (nodesToSync === 0)
- return html`
`
-
- return html`
-
-
-
${nodesSynced} / ${nodesToSync}
-
- `
+ render() {//{{{
+ this.elProgress.max = this.state.nodesToSync
+ this.elProgress.value = this.state.nodesSynced
+ this.elCount.innerText = `${this.state.nodesSynced} / ${this.state.nodesToSync}`
}//}}}
+ setSyncState(state) {// {{{
+ if (state)
+ this.classList.add('show')
+ else
+ setTimeout(() => this.classList.remove('show'), 1500)
+ }// }}}
}
+customElements.define('n2-syncprogress', N2SyncProgress)
// vim: foldmethod=marker
diff --git a/static/js/tree.mjs b/static/js/tree.mjs
new file mode 100644
index 0000000..1da5dee
--- /dev/null
+++ b/static/js/tree.mjs
@@ -0,0 +1,451 @@
+import { ROOT_NODE } from 'node_store'
+import { CustomHTMLElement } from './lib/custom_html_element.mjs'
+
+export class N2Tree extends CustomHTMLElement {
+ static {// {{{
+ this.tmpl = document.createElement('template')
+ this.tmpl.innerHTML = `
+
+
+
+
+
+
+ `
+ }// }}}
+
+ constructor() {// {{{
+ super()
+
+ this.id = 'tree-nodes'
+ this.tabIndex = 0
+
+ this.treeNodeComponents = {}
+ this.treeTrunk = []
+ this.expandedNodes = {} // keyed on UUID
+ this.selectedNode = null
+ this.rendered = false
+
+ this.addEventListener('keydown', event => this.keyHandler(event))
+ 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.populateFirstLevel()
+ }// }}}
+ render() {// {{{
+ if (this.rendered)
+ alert('Tree should only be rendered once.')
+
+ for (const node of this.treeTrunk) {
+ const treenode = new N2TreeNode(this, node)
+ this.treeNodeComponents[node.UUID] = treenode
+ this.elTreenodes.appendChild(treenode.render())
+ }
+
+ this.rendered = true
+ return this
+ }// }}}
+ reset() {
+ console.log('tree reset')
+ this.treeNodeComponents = {}
+ this.treeTrunk = []
+ this.rendered = false
+ this.elTreenodes.replaceChildren()
+ this.populateFirstLevel()
+ }
+ 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: false, 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: false, dontExpand: true })
+ return
+ }
+
+ _mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getSiblingBefore()?.UUID, dontPush: false, 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: false, dontExpand: true })
+ return
+ }
+
+ if (n.isLastSibling()) {
+ const nextNode = this.getParentWithNextSibling(n)
+ _mbus.dispatch("GO_TO_NODE", { nodeUUID: nextNode?.UUID, dontPush: false, dontExpand: true })
+ return
+ }
+
+ _mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getSiblingAfter()?.UUID, dontPush: false, 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: false, dontExpand: true })
+ return
+ }
+
+ if (siblingBefore !== null && siblingExpanded && siblingBefore.hasChildren()) {
+ _mbus.dispatch("GO_TO_NODE", { nodeUUID: siblingBefore.Children[siblingBefore.Children.length - 1]?.UUID, dontPush: false, dontExpand: true })
+ return
+ }
+
+ if (siblingBefore) {
+ _mbus.dispatch("GO_TO_NODE", { nodeUUID: siblingBefore.UUID, dontPush: false, 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: false, dontExpand: true })
+ return
+ }
+
+ if (nodeExpanded && n.isLastSibling() && !n.hasChildren()) {
+ const wantedNode = this.getParentWithNextSibling(n)
+ _mbus.dispatch("GO_TO_NODE", { nodeUUID: wantedNode?.UUID, dontPush: false, 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: false, 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: false, 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: false, 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: false, dontExpand: true })
+ } else
+ _mbus.dispatch("GO_TO_NODE", { nodeUUID: root.Children[root.Children.length - 1]?.UUID, dontPush: false, 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' })
+ }// }}}
+}
+customElements.define('n2-tree', N2Tree)
+
+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
deleted file mode 100644
index a1ae783..0000000
--- a/static/less/notes2.less
+++ /dev/null
@@ -1,350 +0,0 @@
-@import "theme.less";
-
-html {
- background-color: #fff;
-}
-
-#notes2 {
- min-height: 100vh;
-
- display: grid;
- grid-template-areas:
- "tree crumbs"
- "tree sync"
- "tree name"
- "tree content"
- //"tree checklist"
- //"tree schedule"
- //"tree files"
- "tree blank"
- ;
- grid-template-columns: min-content 1fr;
- grid-template-rows:
- 48px
- 56px
- 48px
- min-content
- 1fr;
-
-
- @media only screen and (max-width: 600px) {
- grid-template-areas:
- "crumbs"
- "sync"
- "name"
- "content"
- //"checklist"
- //"schedule"
- //"files"
- "blank"
- ;
- grid-template-columns: 1fr;
-
- #tree {
- display: none;
- }
- }
-}
-
-#tree {
- grid-area: tree;
- padding: 16px 32px;
- background-color: #333;
- 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-bottom: 8px;
- margin-left: 24px;
- margin-right: 24px;
- img {
- width: 128px;
- left: -20px;
-
- }
- }
-
- .icons {
- display: flex;
- justify-content: center;
- margin-bottom: 32px;
- gap: 8px;
- }
-
- .node {
- display: grid;
- grid-template-columns: 24px min-content;
- grid-template-rows:
- min-content
- 1fr;
- margin-top: 12px;
-
-
- .expand-toggle {
- user-select: none;
- img {
- width: 16px;
- height: 16px;
- }
- }
-
- .name {
- white-space: nowrap;
- cursor: pointer;
- user-select: none;
-
- &:hover {
- color: @color1;
- }
- &.selected {
- color: @color1;
- font-weight: bold;
- }
-
- }
-
- .children {
- padding-left: 24px;
- margin-left: 8px;
- border-left: 1px solid #444;
- grid-column: 1 / -1;
-
- &.collapsed {
- display: none;
- }
- }
- }
-}
-
-#crumbs {
- grid-area: crumbs;
- display: grid;
- align-items: start;
- justify-items: center;
- margin: 0px 16px;
-
- .crumbs {
- background: #e4e4e4;
- display: flex;
- flex-wrap: wrap;
- padding: 8px 16px;
- background: #e4e4e4;
- color: #333;
- border-bottom-left-radius: 5px;
- border-bottom-right-radius: 5px;
-
- &.node-modified {
- background-color: @color1;
- color: @color2;
- .crumb:after {
- color: @color2;
- }
- }
-
- .crumb {
- margin-right: 8px;
- cursor: pointer;
- user-select: none;
- -webkit-tap-highlight-color: transparent;
- }
-
- .crumb:after {
- content: "•";
- margin-left: 8px;
- color: @color1
- }
-
- .crumb:last-child {
- margin-right: 0;
- }
- .crumb:last-child:after {
- content: '';
- margin-left: 0px;
- }
-
- }
-
-}
-
-#sync-progress {
- grid-area: sync;
- display: grid;
- justify-items: center;
-
- width: 100%;
- height: 56px;
- position: relative;
-
- progress {
- width: 100%;
- padding: 0 7px;
- max-width: 900px;
- height: 16px;
- border-radius: 4px;
- }
-
- progress[value]::-webkit-progress-bar {
- background-color: #eee;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25) inset;
- border-radius: 4px;
- }
-
- progress[value]::-moz-progress-bar {
- background-color: #eee;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25) inset;
- border-radius: 4px;
- }
-
- progress[value]::-webkit-progress-value {
- background: rgb(186,95,89);
- background: linear-gradient(180deg, rgba(186,95,89,1) 0%, rgba(254,95,85,1) 50%, rgba(186,95,89,1) 100%);
- border-radius: 4px;
- }
-
- // TODO: style the progress value for Firefox
- progress[value]::-moz-progress-value {
- background: rgb(186,95,89);
- background: linear-gradient(180deg, rgba(186,95,89,1) 0%, rgba(254,95,85,1) 50%, rgba(186,95,89,1) 100%);
- border-radius: 4px;
- }
-
- .count {
- width: min-content;
- white-space: nowrap;
- margin-top: 0px;
- color: #888;
- position: absolute;
- top: 22px;
- }
-
- &.hidden {
- visibility: hidden;
- opacity: 0;
- transition: visibility 0s 500ms, opacity 500ms linear;
- }
-
-}
-
-#name {
- color: #333;
- font-weight: bold;
- text-align: center;
- font-size: 1.15em;
- margin-top: 0px;
- margin-bottom: 16px;
-}
-
-/* ============================================================= *
- * Textarea replicates the height of an element expanding height *
- * ============================================================= */
-.grow-wrap {
- /* easy way to plop the elements on top of each other and have them both sized based on the tallest one's height */
- display: grid;
- grid-area: content;
- font-size: 1.0em;
-}
-.grow-wrap::after {
- /* Note the weird space! Needed to preventy jumpy behavior */
- content: attr(data-replicated-value) " ";
-
- /* This is how textarea text behaves */
- width: calc(100% - 32px);
- max-width: 900px;
- white-space: pre-wrap;
- word-wrap: break-word;
- background: rgba(0, 255, 255, 0.5);
- justify-self: center;
-
- /* Hidden from view, clicks, and screen readers */
- visibility: hidden;
-}
-.grow-wrap > textarea {
- /* You could leave this, but after a user resizes, then it ruins the auto sizing */
- resize: none;
-
- /* Firefox shows scrollbar on growth, you can hide like this. */
- overflow: hidden;
-}
-.grow-wrap > textarea,
-.grow-wrap::after {
- /* Identical styling required!! */
- padding: 0.5rem;
- font: inherit;
-
- /* Place on top of each other */
- grid-area: 1 / 1 / 2 / 2;
-}
-/* ============================================================= */
-
-#node-content {
- justify-self: center;
- word-wrap: break-word;
- font-family: monospace;
- color: #333;
- width: calc(100% - 32px);
- max-width: 900px;
- resize: none;
- border: none;
- outline: none;
-
- &:invalid {
- background: #f5f5f5;
- padding-top: 16px;
- }
-}
-
-#blank {
- grid-area: blank;
- height: 32px;
-}
-
-dialog.op {
- &::backdrop {
- background: rgba(0, 0, 0, 0.5);
- }
-
- .header {
- font-weight: bold;
- margin-top: 16px;
-
- &:first-child {
- margin-top: 0px;
- }
- }
-
-}
-
-#op-search {
- .results {
- display: grid;
- grid-template-columns: min-content min-content;
- grid-gap: 6px 16px;
-
- div {
- white-space: nowrap;
- }
-
-
- .ancestors {
- display: flex;
-
- .ancestor::after {
- content: ">";
- margin: 0px 8px;
- color: #a00;
- }
-
- .ancestor:last-child::after {
- content: "";
- }
- }
- }
-}
diff --git a/static/less/theme.less b/static/less/theme.less
index 255a49c..b9c47ed 100644
--- a/static/less/theme.less
+++ b/static/less/theme.less
@@ -1,3 +1,5 @@
-@color1: #fe5f55;
-@color2: #efede8;
-@color3: #666;
+:root {
+ --color1: #fe5f55;
+ --color2: #efede8;
+ --color3: #666;
+}
diff --git a/static/service_worker.js b/static/service_worker.js
index 6c77241..c48c162 100644
--- a/static/service_worker.js
+++ b/static/service_worker.js
@@ -6,13 +6,6 @@ const CACHED_ASSETS = [
'/css/{{ .VERSION }}/main.css',
'/css/{{ .VERSION }}/notes2.css',
- '/js/{{ .VERSION }}/lib/preact/preact.mjs',
- '/js/{{ .VERSION }}/lib/preact/devtools.mjs',
- '/js/{{ .VERSION }}/lib/signals/signals.mjs',
- '/js/{{ .VERSION }}/lib/signals/signals-core.mjs',
- '/js/{{ .VERSION }}/lib/preact/hooks.mjs',
- '/js/{{ .VERSION }}/lib/preact/debug.mjs',
- '/js/{{ .VERSION }}/lib/htm/htm.mjs',
'/js/{{ .VERSION }}/lib/fullcalendar.min.js',
'/js/{{ .VERSION }}/lib/node_modules/marked/marked.min.js',
'/js/{{ .VERSION }}/lib/sjcl.js',
diff --git a/views/layouts/main.gotmpl b/views/layouts/main.gotmpl
index 123fc68..b3b5514 100644
--- a/views/layouts/main.gotmpl
+++ b/views/layouts/main.gotmpl
@@ -2,8 +2,26 @@
-
+
+
-
diff --git a/views/pages/notes2.gotmpl b/views/pages/notes2.gotmpl
index f633692..77b74a6 100644
--- a/views/pages/notes2.gotmpl
+++ b/views/pages/notes2.gotmpl
@@ -1,35 +1,33 @@
{{ define "page" }}
-
+
+
{{ end }}