Compare commits
No commits in common. "nativejs" and "main" have entirely different histories.
19 changed files with 1213 additions and 1553 deletions
14
main.go
14
main.go
|
|
@ -25,7 +25,7 @@ import (
|
||||||
|
|
||||||
const VERSION = "v1"
|
const VERSION = "v1"
|
||||||
const CONTEXT_USER = 1
|
const CONTEXT_USER = 1
|
||||||
const SYNC_PAGINATION = 200
|
const SYNC_PAGINATION = 100
|
||||||
|
|
||||||
var (
|
var (
|
||||||
FlagGenerate bool
|
FlagGenerate bool
|
||||||
|
|
@ -269,11 +269,9 @@ func actionSyncFromServer(w http.ResponseWriter, r *http.Request) { // {{{
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
Log.Debug("/sync/from_server", "num_nodes", len(nodes), "maxSeq", maxSeq)
|
Log.Debug("/sync/from_server", "num_nodes", len(nodes), "maxSeq", maxSeq)
|
||||||
foo, _ := json.Marshal(nodes)
|
foo, _ := json.Marshal(nodes)
|
||||||
os.WriteFile(fmt.Sprintf("/tmp/nodes-%d.json", offset), foo, 0644)
|
os.WriteFile(fmt.Sprintf("/tmp/nodes-%d.json", offset), foo, 0644)
|
||||||
*/
|
|
||||||
|
|
||||||
j, _ := json.Marshal(struct {
|
j, _ := json.Marshal(struct {
|
||||||
OK bool
|
OK bool
|
||||||
|
|
@ -290,6 +288,7 @@ func actionSyncFromServerCount(w http.ResponseWriter, r *http.Request) { // {{{
|
||||||
user := getUser(r)
|
user := getUser(r)
|
||||||
changedFrom, _ := strconv.Atoi(r.PathValue("sequence"))
|
changedFrom, _ := strconv.Atoi(r.PathValue("sequence"))
|
||||||
|
|
||||||
|
Log.Debug("FOO", "UUID", user.ClientUUID, "changedFrom", changedFrom)
|
||||||
count, err := NodesCount(user.UserID, uint64(changedFrom), user.ClientUUID)
|
count, err := NodesCount(user.UserID, uint64(changedFrom), user.ClientUUID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Log.Error("/sync/from_server/count", "error", err)
|
Log.Error("/sync/from_server/count", "error", err)
|
||||||
|
|
@ -335,14 +334,9 @@ func actionSyncToServer(w http.ResponseWriter, r *http.Request) { // {{{
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = db.Exec(`CALL add_nodes($1, $2, $3::jsonb)`, user.UserID, user.ClientUUID, request.NodeData)
|
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]any{
|
responseData(w, map[string]interface{}{
|
||||||
"OK": true,
|
"OK": true,
|
||||||
})
|
})
|
||||||
} // }}}
|
} // }}}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,37 @@
|
||||||
@import "theme.css";
|
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
display: grid;
|
display: grid;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
margin-top: 128px;
|
margin-top: 128px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#logo {
|
#logo {
|
||||||
margin-bottom: 48px;
|
margin-bottom: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#box {
|
#box {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-gap: 16px 0;
|
grid-gap: 16px 0;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
padding: 48px 0px;
|
padding: 48px 0px;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
box-shadow: 0px 20px 52px -33px rgba(0,0,0,0.75);
|
box-shadow: 0px 20px 52px -33px rgba(0, 0, 0, 0.75);
|
||||||
border-left: 8px solid var(--color3);
|
border-left: 8px solid #666;
|
||||||
|
}
|
||||||
input {
|
#box input {
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
width: calc(100% - 64px);
|
width: calc(100% - 64px);
|
||||||
border: 1px solid #aaa;
|
border: 1px solid #aaa;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
#box button {
|
||||||
button {
|
padding: 6px 16px;
|
||||||
padding: 6px 16px;
|
font-size: 1em;
|
||||||
font-size: 1em;
|
border-radius: 4px;
|
||||||
border-radius: 4px;
|
border: none;
|
||||||
border: none;
|
background-color: #fe5f55;
|
||||||
background-color: var(--color1);
|
color: #fff;
|
||||||
color: #fff;
|
}
|
||||||
}
|
#box #error {
|
||||||
|
color: #c33;
|
||||||
#error {
|
margin-top: 16px;
|
||||||
color: #c33;
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,23 @@
|
||||||
@import "theme.css";
|
|
||||||
|
|
||||||
html {
|
html {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: var(--color2);
|
background: #efede8;
|
||||||
font-family: "Liberation Mono", monospace;
|
font-family: "Liberation Mono", monospace;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
*,
|
*,
|
||||||
*:before,
|
*:before,
|
||||||
*:after {
|
*:after {
|
||||||
box-sizing: inherit;
|
box-sizing: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
*:focus {
|
*:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
[onClick] {
|
[onClick] {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,361 +1,264 @@
|
||||||
@import "theme.css";
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--content-width: 900px;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
html {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
#notes2 {
|
#notes2 {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
display: grid;
|
||||||
display: grid;
|
grid-template-areas: "tree crumbs" "tree sync" "tree name" "tree content" "tree blank";
|
||||||
grid-template-areas:
|
grid-template-columns: min-content 1fr;
|
||||||
"tree crumbs"
|
grid-template-rows: 48px 56px 48px min-content 1fr;
|
||||||
"tree name"
|
}
|
||||||
"tree sync"
|
@media only screen and (max-width: 600px) {
|
||||||
"tree content"
|
#notes2 {
|
||||||
/*
|
grid-template-areas: "crumbs" "sync" "name" "content" "blank";
|
||||||
"tree checklist"
|
grid-template-columns: 1fr;
|
||||||
"tree files"
|
}
|
||||||
*/
|
#notes2 #tree {
|
||||||
"tree blank"
|
display: none;
|
||||||
;
|
}
|
||||||
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 {
|
#tree {
|
||||||
grid-area: tree;
|
grid-area: tree;
|
||||||
display: grid;
|
padding: 16px 32px;
|
||||||
padding: 16px 0px 16px 16px;
|
background-color: #333;
|
||||||
color: #ddd;
|
color: #ddd;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
/* Over crumbs shadow */
|
border-left: 2px solid #333;
|
||||||
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 {
|
||||||
#tree-nodes {
|
border-left: 2px solid #FE5F55;
|
||||||
padding: 16px 32px;
|
}
|
||||||
background-color: #333;
|
#tree #logo {
|
||||||
border-radius: 8px;
|
display: grid;
|
||||||
box-shadow: 5px 5px 10px -5px rgba(0, 0, 0, 0.75);
|
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 {
|
#crumbs {
|
||||||
grid-area: crumbs;
|
grid-area: crumbs;
|
||||||
display: grid;
|
display: grid;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
height: min-content;
|
margin: 0px 16px;
|
||||||
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 {
|
||||||
n2-syncprogress {
|
display: flex;
|
||||||
--radius: 8px;
|
flex-wrap: wrap;
|
||||||
|
padding: 8px 16px;
|
||||||
display: grid;
|
background: #e4e4e4;
|
||||||
grid-area: sync;
|
color: #333;
|
||||||
display: grid;
|
border-bottom-left-radius: 5px;
|
||||||
justify-items: center;
|
border-bottom-right-radius: 5px;
|
||||||
align-items: center;
|
}
|
||||||
|
#crumbs .crumbs.node-modified {
|
||||||
position: relative;
|
background-color: #fe5f55;
|
||||||
|
color: #efede8;
|
||||||
opacity: 0;
|
}
|
||||||
transition: height 0s 500ms, opacity 500ms linear, visibility 0s 500ms;
|
#crumbs .crumbs.node-modified .crumb:after {
|
||||||
|
color: #efede8;
|
||||||
&.show {
|
}
|
||||||
opacity: 1;
|
#crumbs .crumbs .crumb {
|
||||||
transition: visibility, height 0s, opacity 500ms linear;
|
margin-right: 8px;
|
||||||
}
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
progress {
|
-webkit-tap-highlight-color: transparent;
|
||||||
width: calc(100% - 32px);
|
}
|
||||||
max-width: var(--content-width);
|
#crumbs .crumbs .crumb:after {
|
||||||
height: 24px;
|
content: "•";
|
||||||
border-radius: 8px;
|
margin-left: 8px;
|
||||||
}
|
color: #fe5f55;
|
||||||
|
}
|
||||||
.count {
|
#crumbs .crumbs .crumb:last-child {
|
||||||
position: absolute;
|
margin-right: 0;
|
||||||
top: 16px;
|
}
|
||||||
width: 100%;
|
#crumbs .crumbs .crumb:last-child:after {
|
||||||
white-space: nowrap;
|
content: '';
|
||||||
color: #888;
|
margin-left: 0px;
|
||||||
text-align: center;
|
}
|
||||||
font-size: 12pt;
|
#sync-progress {
|
||||||
font-weight: bold;
|
grid-area: sync;
|
||||||
}
|
display: grid;
|
||||||
|
justify-items: center;
|
||||||
progress[value]::-webkit-progress-bar {
|
width: 100%;
|
||||||
background-color: #eee;
|
height: 56px;
|
||||||
box-shadow: 0 2px var(--radius) rgba(0, 0, 0, 0.25) inset;
|
position: relative;
|
||||||
border-radius: var(--radius);
|
}
|
||||||
}
|
#sync-progress progress {
|
||||||
|
width: 100%;
|
||||||
progress[value]::-moz-progress-bar {
|
padding: 0 7px;
|
||||||
background-color: #eee;
|
max-width: 900px;
|
||||||
box-shadow: 0 2px var(--radius) rgba(0, 0, 0, 0.25) inset;
|
height: 16px;
|
||||||
border-radius: var(--radius);
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
#sync-progress progress[value]::-webkit-progress-bar {
|
||||||
progress[value]::-webkit-progress-value {
|
background-color: #eee;
|
||||||
background: rgb(186, 95, 89);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25) inset;
|
||||||
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;
|
||||||
border-radius: var(--radius);
|
}
|
||||||
}
|
#sync-progress progress[value]::-moz-progress-bar {
|
||||||
|
background-color: #eee;
|
||||||
progress[value]::-moz-progress-value {
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25) inset;
|
||||||
background: rgb(186, 95, 89);
|
border-radius: 4px;
|
||||||
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);
|
#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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================= */
|
/* ============================================================= */
|
||||||
|
#node-content {
|
||||||
n2-nodeui {
|
justify-self: center;
|
||||||
margin-bottom: 32px;
|
word-wrap: break-word;
|
||||||
|
font-family: monospace;
|
||||||
.el-name {
|
color: #333;
|
||||||
color: #333;
|
width: calc(100% - 32px);
|
||||||
font-weight: bold;
|
max-width: 900px;
|
||||||
text-align: center;
|
resize: none;
|
||||||
font-size: 1.15em;
|
border: none;
|
||||||
margin-top: 8px;
|
outline: none;
|
||||||
margin-bottom: 0px;
|
}
|
||||||
}
|
#node-content:invalid {
|
||||||
|
background: #f5f5f5;
|
||||||
.el-node-content {
|
padding-top: 16px;
|
||||||
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 {
|
#blank {
|
||||||
grid-area: blank;
|
grid-area: blank;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
}
|
}
|
||||||
|
dialog.op::backdrop {
|
||||||
dialog.op {
|
background: rgba(0, 0, 0, 0.5);
|
||||||
&::backdrop {
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
font-weight: bold;
|
|
||||||
margin-top: 16px;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-top: 0px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
dialog.op .header {
|
||||||
#op-search {
|
font-weight: bold;
|
||||||
.results {
|
margin-top: 16px;
|
||||||
display: grid;
|
}
|
||||||
grid-template-columns: min-content min-content;
|
dialog.op .header:first-child {
|
||||||
grid-gap: 6px 16px;
|
margin-top: 0px;
|
||||||
|
}
|
||||||
div {
|
#op-search .results {
|
||||||
white-space: nowrap;
|
display: grid;
|
||||||
}
|
grid-template-columns: min-content min-content;
|
||||||
|
grid-gap: 6px 16px;
|
||||||
|
}
|
||||||
.ancestors {
|
#op-search .results div {
|
||||||
display: flex;
|
white-space: nowrap;
|
||||||
|
}
|
||||||
.ancestor::after {
|
#op-search .results .ancestors {
|
||||||
content: ">";
|
display: flex;
|
||||||
margin: 0px 8px;
|
}
|
||||||
color: #a00;
|
#op-search .results .ancestors .ancestor::after {
|
||||||
}
|
content: ">";
|
||||||
|
margin: 0px 8px;
|
||||||
.ancestor:last-child::after {
|
color: #a00;
|
||||||
content: "";
|
}
|
||||||
}
|
#op-search .results .ancestors .ancestor:last-child::after {
|
||||||
}
|
content: "";
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
:root {
|
|
||||||
--color1: #fe5f55;
|
|
||||||
--color2: #efede8;
|
|
||||||
--color3: #666;
|
|
||||||
}
|
|
||||||
|
|
@ -1,329 +0,0 @@
|
||||||
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 = `
|
|
||||||
<a data-el="link"></a>
|
|
||||||
`
|
|
||||||
}// }}}
|
|
||||||
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 = `<dialog id="${this.id}" class="op">${html}</dialog>`
|
|
||||||
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(`
|
|
||||||
<div class="header">Search</div>
|
|
||||||
<div>
|
|
||||||
<input type="text" />
|
|
||||||
</div>
|
|
||||||
<div class="header">Results</div>
|
|
||||||
<div class="results"></div>
|
|
||||||
`)
|
|
||||||
|
|
||||||
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(`<div class="ancestor">${a.data.Name}</div>`)
|
|
||||||
div[0].addEventListener('click', () => _notes2.current.goToNode(a.UUID))
|
|
||||||
return div[0]
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
const div = tmpl(`<div>${r.name}</div>`)
|
|
||||||
div[0].addEventListener('click', () => _notes2.current.goToNode(r.uuid))
|
|
||||||
rs.push(...div)
|
|
||||||
|
|
||||||
const ancDev = tmpl('<div class="ancestors"></div>')
|
|
||||||
ancDev[0].append(...ancestors)
|
|
||||||
rs.push(ancDev[0])
|
|
||||||
}
|
|
||||||
this.get('.results').replaceChildren(...rs)
|
|
||||||
}// }}}
|
|
||||||
}
|
|
||||||
|
|
||||||
// vim: foldmethod=marker
|
|
||||||
|
|
@ -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)
|
|
||||||
}// }}}
|
|
||||||
}
|
|
||||||
|
|
@ -1,48 +1,17 @@
|
||||||
export class MessageBus {
|
export class MessageBus {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.log = false
|
|
||||||
this.bus = new EventTarget()
|
this.bus = new EventTarget()
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe(eventName, fn) {
|
subscribe(eventName, fn) {
|
||||||
if (this.log) {
|
this.bus.addEventListener(eventName, fn)
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
unsubscribe(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)
|
this.bus.removeEventListener(eventName, fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(eventName, data, callback) {
|
dispatch(eventName, data) {
|
||||||
if (this.log) {
|
this.bus.dispatchEvent(new CustomEvent(eventName, { detail: data }))
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,335 @@
|
||||||
|
import { h, Component, createRef } from 'preact'
|
||||||
|
import htm from 'htm'
|
||||||
|
import { signal } from 'preact/signals'
|
||||||
import { ROOT_NODE } from 'node_store'
|
import { ROOT_NODE } from 'node_store'
|
||||||
import { CustomHTMLElement } from './lib/custom_html_element.mjs'
|
import { SyncProgress } from 'sync'
|
||||||
|
const html = htm.bind(h)
|
||||||
|
|
||||||
export class N2NodeUI extends CustomHTMLElement {
|
export class NodeUI extends Component {
|
||||||
static {// {{{
|
constructor(props) {//{{{
|
||||||
this.tmpl = document.createElement('template')
|
super(props)
|
||||||
this.tmpl.innerHTML = `
|
this.menu = signal(false)
|
||||||
<div data-el="name"></div>
|
this.node = signal(null)
|
||||||
<textarea data-el="node-content" required rows=1></textarea>
|
this.nodeContent = createRef()
|
||||||
`
|
this.nodeProperties = createRef()
|
||||||
}// }}}
|
this.nodeModified = signal(false)
|
||||||
|
this.keys = signal([])
|
||||||
constructor() {// {{{
|
this.page = signal('node')
|
||||||
super()
|
this.crumbs = []
|
||||||
this.node = null
|
this.syncProgress = createRef()
|
||||||
|
window.addEventListener('popstate', evt => {
|
||||||
this.style.display = 'contents'
|
if (evt.state?.hasOwnProperty('nodeUUID'))
|
||||||
|
_notes2.current.goToNode(evt.state.nodeUUID, true)
|
||||||
_mbus.subscribe('NODE_UI_OPEN', event => {
|
else
|
||||||
this.node = event.detail.data
|
_notes2.current.goToNode('00000000-0000-0000-0000-000000000000', true)
|
||||||
this.render()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
_mbus.subscribe('NODE_MODIFIED', () => {
|
window.addEventListener('keydown', evt => this.keyHandler(evt))
|
||||||
document.querySelector('#crumbs .crumbs')?.classList.add('node-modified')
|
}//}}}
|
||||||
})
|
render() {//{{{
|
||||||
|
if (this.node.value === null)
|
||||||
_mbus.subscribe('NODE_UNMODIFIED', () => {
|
return
|
||||||
document.querySelector('#crumbs .crumbs')?.classList.remove('node-modified')
|
|
||||||
})
|
const node = this.node.value
|
||||||
|
document.title = node.get('Name')
|
||||||
this.elNodeContent.addEventListener('input', event => this.contentChanged(event))
|
|
||||||
}// }}}
|
const nodeModified = this.nodeModified.value ? 'node-modified' : ''
|
||||||
render() {// {{{
|
|
||||||
this.elName.innerText = this.node?.get('Name') ?? ''
|
|
||||||
this.elNodeContent.value = this.node?.get('Content') ?? ''
|
const crumbDivs = [
|
||||||
}// }}}
|
html`<div class="crumb" onclick=${() => _notes2.current.goToNode(ROOT_NODE)}>Start</div>`
|
||||||
takeFocus() {// {{{
|
]
|
||||||
this.elNodeContent.focus()
|
for (let i = this.crumbs.length - 1; i >= 0; i--) {
|
||||||
}// }}}
|
const crumbNode = this.crumbs[i]
|
||||||
|
crumbDivs.push(html`<div class="crumb" onclick=${() => _notes2.current.goToNode(crumbNode.UUID)}>${crumbNode.get('Name')}</div>`)
|
||||||
contentChanged(event) {//{{{
|
}
|
||||||
this.node.setContent(event.target.value)
|
if (node.UUID !== ROOT_NODE)
|
||||||
|
crumbDivs.push(
|
||||||
|
html`<div class="crumb" onclick=${() => _notes2.current.goToNode(node.UUID)}>${node.get('Name')}</div>`
|
||||||
|
)
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div id="crumbs" onclick=${() => this.saveNode()}>
|
||||||
|
<div class="crumbs ${nodeModified}">
|
||||||
|
${crumbDivs}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<${SyncProgress} ref=${this.syncProgress} />
|
||||||
|
<div id="name">${node.get('Name')}</div>
|
||||||
|
<${NodeContent} key=${node.UUID} node=${node} ref=${this.nodeContent} />
|
||||||
|
<div id="blank"></div>
|
||||||
|
`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
let crumbs = [
|
||||||
|
html`<div class="crumb" onclick=${() => this.goToNode(0)}>Start</div>`
|
||||||
|
]
|
||||||
|
|
||||||
|
crumbs = crumbs.concat(node.Crumbs.slice(0).map(node =>
|
||||||
|
html`<div class="crumb" onclick=${() => this.goToNode(node.ID)}>${node.Name}</div>`
|
||||||
|
).reverse())
|
||||||
|
|
||||||
|
|
||||||
|
// Page to display
|
||||||
|
let page = ''
|
||||||
|
switch (this.page.value) {
|
||||||
|
case 'node':
|
||||||
|
if (node.ID === 0) {
|
||||||
|
page = html`
|
||||||
|
<div style="cursor: pointer; color: #000; text-align: center;" onclick=${() => { this.page.value = 'schedule-events' }}>Schedule events</div>
|
||||||
|
${children.length > 0 ? html`<div class="child-nodes">${children}</div><div id="notes-version">Notes version ${window._VERSION}</div>` : html``}
|
||||||
|
`
|
||||||
|
} else {
|
||||||
|
let padlock = ''
|
||||||
|
if (node.CryptoKeyID > 0)
|
||||||
|
padlock = html`<img src="/images/${window._VERSION}/padlock-black.svg" style="height: 24px;" />`
|
||||||
|
|
||||||
|
page = html`
|
||||||
|
${children.length > 0 ? html`<div class="child-nodes">${children}</div>` : html``}
|
||||||
|
<div class="node-name">
|
||||||
|
${node.Name} ${padlock}
|
||||||
|
</div>
|
||||||
|
<${NodeContent} key=${node.ID} node=${node} ref=${this.nodeContent} />
|
||||||
|
<${NodeEvents} events=${node.ScheduleEvents.value} />
|
||||||
|
<${Checklist} ui=${this} groups=${node.ChecklistGroups} />
|
||||||
|
<${NodeFiles} node=${this.node.value} />
|
||||||
|
`
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'upload':
|
||||||
|
page = html`<${UploadUI} nodeui=${this} />`
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'node-properties':
|
||||||
|
page = html`<${NodeProperties} ref=${this.nodeProperties} nodeui=${this} />`
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'keys':
|
||||||
|
page = html`<${Keys} nodeui=${this} />`
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'profile-settings':
|
||||||
|
page = html`<${ProfileSettings} nodeui=${this} />`
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'search':
|
||||||
|
page = html`<${Search} nodeui=${this} />`
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'schedule-events':
|
||||||
|
page = html`<${ScheduleEventList} nodeui=${this} />`
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
const menu = () => (this.menu.value ? html`<${Menu} nodeui=${this} />` : null)
|
||||||
|
const checklist = () =>
|
||||||
|
html`
|
||||||
|
<div class="checklist" onclick=${evt => { evt.stopPropagation(); this.toggleChecklist() }}>
|
||||||
|
<img src="/images/${window._VERSION}/${this.showChecklist() ? 'checklist-on.svg' : 'checklist-off.svg'}" />
|
||||||
|
</div>`
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<${menu} />
|
||||||
|
<!--header class="${modified}" onclick=${() => this.saveNode()}>
|
||||||
|
<div class="tree"><img src="/images/${window._VERSION}/tree.svg" onclick=${() => document.getElementById('app').classList.toggle('toggle-tree')} /></div>
|
||||||
|
<div class="name">Notes</div>
|
||||||
|
<div class="markdown" onclick=${evt => { evt.stopPropagation(); this.toggleMarkdown() }}><img src="/images/${window._VERSION}/${node.RenderMarkdown.value ? 'markdown.svg' : 'markdown-hollow.svg'}" /></div>
|
||||||
|
<${checklist} />
|
||||||
|
<div class="search" onclick=${evt => { evt.stopPropagation(); this.showPage('search') }}><img src="/images/${window._VERSION}/search.svg" /></div>
|
||||||
|
<div class="add" onclick=${evt => this.createNode(evt)}><img src="/images/${window._VERSION}/add.svg" /></div>
|
||||||
|
<div class="keys" onclick=${evt => { evt.stopPropagation(); this.showPage('keys') }}><img src="/images/${window._VERSION}/padlock.svg" /></div>
|
||||||
|
<div class="menu" onclick=${evt => this.showMenu(evt)}>☰</div>
|
||||||
|
</header-->
|
||||||
|
|
||||||
|
<div style="display: grid; justify-items: center;">
|
||||||
|
<div id="crumbs">
|
||||||
|
<div class="crumbs">${crumbs}</crumbs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${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`
|
||||||
|
<div id="node-content" class="node-content encrypted">${err.message}</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
let element
|
||||||
|
if (node.RenderMarkdown.value)
|
||||||
|
element = html`<${MarkdownContent} key='markdown-content' content=${content} />`
|
||||||
|
else
|
||||||
|
*/
|
||||||
|
const element = html`
|
||||||
|
<div class="grow-wrap">
|
||||||
|
<textarea id="node-content" class="node-content" ref=${this.contentDiv} oninput=${evt => this.contentChanged(evt)} required rows=1>${content}</textarea>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
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
|
||||||
}//}}}
|
}//}}}
|
||||||
isModified() {// {{{
|
|
||||||
return this.node?.isModified()
|
|
||||||
}// }}}
|
|
||||||
}
|
}
|
||||||
customElements.define('n2-nodeui', N2NodeUI)
|
|
||||||
|
|
||||||
export class Node {
|
export class Node {
|
||||||
static sort(a, b) {//{{{
|
static sort(a, b) {//{{{
|
||||||
|
|
@ -54,22 +337,10 @@ export class Node {
|
||||||
if (a.data.Name > b.data.Name) return 0
|
if (a.data.Name > b.data.Name) return 0
|
||||||
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) {//{{{
|
constructor(nodeData, level) {//{{{
|
||||||
|
|
||||||
this.Level = level
|
this.Level = level
|
||||||
this.data = nodeData
|
this.data = nodeData
|
||||||
|
|
||||||
this.UUID = nodeData.UUID
|
this.UUID = nodeData.UUID
|
||||||
|
|
||||||
// Toplevel nodes are normalized to have the ROOT_NODE as parent.
|
// Toplevel nodes are normalized to have the ROOT_NODE as parent.
|
||||||
|
|
@ -83,12 +354,13 @@ export class Node {
|
||||||
this.Children = []
|
this.Children = []
|
||||||
this.Ancestors = []
|
this.Ancestors = []
|
||||||
|
|
||||||
|
this._content = this.data.Content
|
||||||
|
this._modified = false
|
||||||
|
|
||||||
this._sibling_before = null
|
this._sibling_before = null
|
||||||
this._sibling_after = null
|
this._sibling_after = null
|
||||||
this._parent = null
|
this._parent = null
|
||||||
|
|
||||||
this.reset()
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
this.RenderMarkdown = signal(nodeData.RenderMarkdown)
|
this.RenderMarkdown = signal(nodeData.RenderMarkdown)
|
||||||
this.Markdown = false
|
this.Markdown = false
|
||||||
|
|
@ -105,10 +377,6 @@ export class Node {
|
||||||
*/
|
*/
|
||||||
}//}}}
|
}//}}}
|
||||||
|
|
||||||
reset() {// {{{
|
|
||||||
this._content = this.data.Content
|
|
||||||
this._modified = false
|
|
||||||
}// }}}
|
|
||||||
get(prop) {//{{{
|
get(prop) {//{{{
|
||||||
return this.data[prop]
|
return this.data[prop]
|
||||||
}//}}}
|
}//}}}
|
||||||
|
|
@ -116,13 +384,13 @@ export class Node {
|
||||||
// '2024-12-17T17:33:48.85939Z
|
// '2024-12-17T17:33:48.85939Z
|
||||||
return new Date(Date.parse(this.data.Updated))
|
return new Date(Date.parse(this.data.Updated))
|
||||||
}//}}}
|
}//}}}
|
||||||
isModified() {// {{{
|
|
||||||
return this._modified
|
|
||||||
}// }}}
|
|
||||||
hasFetchedChildren() {//{{{
|
hasFetchedChildren() {//{{{
|
||||||
return this._children_fetched
|
return this._children_fetched
|
||||||
}//}}}
|
}//}}}
|
||||||
async fetchChildren() {//{{{
|
async fetchChildren() {//{{{
|
||||||
|
if (this._children_fetched)
|
||||||
|
return this.Children
|
||||||
|
|
||||||
this.Children = await nodeStore.getTreeNodes(this.UUID, this.Level + 1)
|
this.Children = await nodeStore.getTreeNodes(this.UUID, this.Level + 1)
|
||||||
this._children_fetched = true
|
this._children_fetched = true
|
||||||
|
|
||||||
|
|
@ -140,8 +408,7 @@ export class Node {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify the tree that all children are fetched and ready to process.
|
// 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
|
return this.Children
|
||||||
}//}}}
|
}//}}}
|
||||||
|
|
@ -168,12 +435,12 @@ export class Node {
|
||||||
if (this.CryptoKeyID != 0 && !this._decrypted)
|
if (this.CryptoKeyID != 0 && !this._decrypted)
|
||||||
this.#decrypt()
|
this.#decrypt()
|
||||||
*/
|
*/
|
||||||
|
this.modified = true
|
||||||
return this._content
|
return this._content
|
||||||
}//}}}
|
}//}}}
|
||||||
|
|
||||||
setContent(new_content) {//{{{
|
setContent(new_content) {//{{{
|
||||||
this._content = new_content
|
this._content = new_content
|
||||||
this._modified = true
|
|
||||||
_mbus.dispatch('NODE_MODIFIED')
|
|
||||||
/* TODO - implement crypto
|
/* TODO - implement crypto
|
||||||
if (this.CryptoKeyID == 0)
|
if (this.CryptoKeyID == 0)
|
||||||
// Logic behind plaintext not being decrypted is that
|
// Logic behind plaintext not being decrypted is that
|
||||||
|
|
@ -188,8 +455,6 @@ export class Node {
|
||||||
this.data.Updated = new Date().toISOString()
|
this.data.Updated = new Date().toISOString()
|
||||||
this._modified = false
|
this._modified = false
|
||||||
|
|
||||||
_mbus.dispatch('NODE_UNMODIFIED')
|
|
||||||
|
|
||||||
// When stored into database and ancestry was changed,
|
// When stored into database and ancestry was changed,
|
||||||
// the ancestry path could be interesting.
|
// the ancestry path could be interesting.
|
||||||
const ancestors = await nodeStore.getNodeAncestry(this)
|
const ancestors = await nodeStore.getNodeAncestry(this)
|
||||||
|
|
@ -197,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
|
// vim: foldmethod=marker
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ export class NodeStore {
|
||||||
this.sendQueue = null
|
this.sendQueue = null
|
||||||
this.nodesHistory = null
|
this.nodesHistory = null
|
||||||
}//}}}
|
}//}}}
|
||||||
initializeDB() {//{{{
|
async initializeDB() {//{{{
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const req = indexedDB.open('notes', 7)
|
const req = indexedDB.open('notes', 7)
|
||||||
|
|
||||||
|
|
@ -78,7 +78,7 @@ export class NodeStore {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}//}}}
|
}//}}}
|
||||||
initializeRootNode() {//{{{
|
async initializeRootNode() {//{{{
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// The root node is a magical node which displays as the first node if none is specified.
|
// 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.
|
// If not already existing, it will be created.
|
||||||
|
|
@ -120,7 +120,7 @@ export class NodeStore {
|
||||||
return n
|
return n
|
||||||
}//}}}
|
}//}}}
|
||||||
|
|
||||||
getAppState(key) {//{{{
|
async getAppState(key) {//{{{
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const trx = this.db.transaction('app_state', 'readonly')
|
const trx = this.db.transaction('app_state', 'readonly')
|
||||||
const appState = trx.objectStore('app_state')
|
const appState = trx.objectStore('app_state')
|
||||||
|
|
@ -135,7 +135,7 @@ export class NodeStore {
|
||||||
getRequest.onerror = (event) => reject(event.target.error)
|
getRequest.onerror = (event) => reject(event.target.error)
|
||||||
})
|
})
|
||||||
}//}}}
|
}//}}}
|
||||||
setAppState(key, value) {//{{{
|
async setAppState(key, value) {//{{{
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const t = this.db.transaction('app_state', 'readwrite')
|
const t = this.db.transaction('app_state', 'readwrite')
|
||||||
|
|
@ -159,8 +159,30 @@ export class NodeStore {
|
||||||
})
|
})
|
||||||
}//}}}
|
}//}}}
|
||||||
|
|
||||||
/*
|
async storeNode(node) {//{{{
|
||||||
upsertNodeRecords(records) {//{{{
|
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) {//{{{
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const t = this.db.transaction('nodes', 'readwrite')
|
const t = this.db.transaction('nodes', 'readwrite')
|
||||||
const nodeStore = t.objectStore('nodes')
|
const nodeStore = t.objectStore('nodes')
|
||||||
|
|
@ -188,11 +210,17 @@ export class NodeStore {
|
||||||
record.modified = 0
|
record.modified = 0
|
||||||
addReq = nodeStore.put(record)
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
// Parent of toplevel nodes is ROOT_NODE in indexedDB.
|
// Parent of toplevel nodes is ROOT_NODE in indexedDB.
|
||||||
// Only the root node has '' as parent.
|
// Only the root node has '' as parent.
|
||||||
|
|
@ -214,7 +242,7 @@ export class NodeStore {
|
||||||
req.onerror = (event) => reject(event.target.error)
|
req.onerror = (event) => reject(event.target.error)
|
||||||
})
|
})
|
||||||
}//}}}
|
}//}}}
|
||||||
search(searchfor, parent) {//{{{
|
async search(searchfor, parent) {//{{{
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const trx = this.db.transaction('nodes', 'readonly')
|
const trx = this.db.transaction('nodes', 'readonly')
|
||||||
const nodeStore = trx.objectStore('nodes')
|
const nodeStore = trx.objectStore('nodes')
|
||||||
|
|
@ -244,55 +272,46 @@ export class NodeStore {
|
||||||
})
|
})
|
||||||
}//}}}
|
}//}}}
|
||||||
|
|
||||||
add(records, objstore) {//{{{
|
async add(records) {//{{{
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
// A nodestore can be provided in order to
|
const t = this.db.transaction('nodes', 'readwrite')
|
||||||
// avoid creating new transactions.
|
const nodeStore = t.objectStore('nodes')
|
||||||
let nodeStore = objstore
|
t.onerror = (event) => {
|
||||||
let t
|
console.log('transaction error', event.target.error)
|
||||||
|
reject(event.target.error)
|
||||||
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.
|
// records is an object, not an array.
|
||||||
|
const promises = []
|
||||||
for (const recordIdx in records) {
|
for (const recordIdx in records) {
|
||||||
const record = records[recordIdx]
|
const record = records[recordIdx]
|
||||||
nodeStore.put(record.data)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve()
|
Promise.all(promises).then(() => resolve())
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.log(e)
|
||||||
reject(e)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}//}}}
|
}//}}}
|
||||||
get(uuid, suppliedNodestore) {//{{{
|
async get(uuid) {//{{{
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// A nodestore can be provided in order to
|
const trx = this.db.transaction('nodes', 'readonly')
|
||||||
// avoid creating new transactions.
|
const nodeStore = trx.objectStore('nodes')
|
||||||
let trx
|
|
||||||
let nodeStore = suppliedNodestore
|
|
||||||
|
|
||||||
if (nodeStore === undefined) {
|
|
||||||
trx = this.db.transaction('nodes', 'readonly')
|
|
||||||
nodeStore = trx.objectStore('nodes')
|
|
||||||
}
|
|
||||||
|
|
||||||
const getRequest = nodeStore.get(uuid)
|
const getRequest = nodeStore.get(uuid)
|
||||||
|
|
||||||
getRequest.onsuccess = (event) => {
|
getRequest.onsuccess = (event) => {
|
||||||
// Node not found in IndexedDB.
|
// Node not found in IndexedDB.
|
||||||
if (event.target.result === undefined) {
|
if (event.target.result === undefined) {
|
||||||
|
|
@ -304,7 +323,7 @@ export class NodeStore {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}//}}}
|
}//}}}
|
||||||
getNodeAncestry(node, accumulated) {//{{{
|
async getNodeAncestry(node, accumulated) {//{{{
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (accumulated === undefined)
|
if (accumulated === undefined)
|
||||||
accumulated = []
|
accumulated = []
|
||||||
|
|
@ -335,11 +354,8 @@ export class NodeStore {
|
||||||
})
|
})
|
||||||
|
|
||||||
}//}}}
|
}//}}}
|
||||||
newTransaction(objectStore, mode) {// {{{
|
|
||||||
return this.db.transaction(objectStore, mode)
|
|
||||||
}// }}}
|
|
||||||
|
|
||||||
nodeCount() {//{{{
|
async nodeCount() {//{{{
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const t = this.db.transaction('nodes', 'readwrite')
|
const t = this.db.transaction('nodes', 'readwrite')
|
||||||
const nodeStore = t.objectStore('nodes')
|
const nodeStore = t.objectStore('nodes')
|
||||||
|
|
@ -356,7 +372,7 @@ class SimpleNodeStore {
|
||||||
this.db = db
|
this.db = db
|
||||||
this.storeName = storeName
|
this.storeName = storeName
|
||||||
}//}}}
|
}//}}}
|
||||||
add(node) {//{{{
|
async add(node) {//{{{
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const t = this.db.transaction(['nodes', this.storeName], 'readwrite')
|
const t = this.db.transaction(['nodes', this.storeName], 'readwrite')
|
||||||
const store = t.objectStore(this.storeName)
|
const store = t.objectStore(this.storeName)
|
||||||
|
|
@ -376,7 +392,7 @@ class SimpleNodeStore {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}//}}}
|
}//}}}
|
||||||
retrieve(limit) {//{{{
|
async retrieve(limit) {//{{{
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const cursorReq = this.db
|
const cursorReq = this.db
|
||||||
.transaction(['nodes', this.storeName], 'readonly')
|
.transaction(['nodes', this.storeName], 'readonly')
|
||||||
|
|
@ -404,7 +420,7 @@ class SimpleNodeStore {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}//}}}
|
}//}}}
|
||||||
delete(keys) {//{{{
|
async delete(keys) {//{{{
|
||||||
const store = this.db
|
const store = this.db
|
||||||
.transaction(['nodes', this.storeName], 'readwrite')
|
.transaction(['nodes', this.storeName], 'readwrite')
|
||||||
.objectStore(this.storeName)
|
.objectStore(this.storeName)
|
||||||
|
|
@ -421,7 +437,7 @@ class SimpleNodeStore {
|
||||||
}
|
}
|
||||||
return Promise.all(promises)
|
return Promise.all(promises)
|
||||||
}//}}}
|
}//}}}
|
||||||
count() {//{{{
|
async count() {//{{{
|
||||||
const store = this.db
|
const store = this.db
|
||||||
.transaction(['nodes', this.storeName], 'readonly')
|
.transaction(['nodes', this.storeName], 'readonly')
|
||||||
.objectStore(this.storeName)
|
.objectStore(this.storeName)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import { signal } from 'preact/signals'
|
||||||
import htm from 'htm'
|
import htm from 'htm'
|
||||||
import { Node, NodeUI } from 'node'
|
import { Node, NodeUI } from 'node'
|
||||||
import { ROOT_NODE } from 'node_store'
|
import { ROOT_NODE } from 'node_store'
|
||||||
import { TreeNative } from 'tree'
|
|
||||||
const html = htm.bind(h)
|
const html = htm.bind(h)
|
||||||
|
|
||||||
export class Notes2 extends Component {
|
export class Notes2 extends Component {
|
||||||
|
|
@ -15,7 +14,6 @@ export class Notes2 extends Component {
|
||||||
startNode: null,
|
startNode: null,
|
||||||
}
|
}
|
||||||
this.op = signal('')
|
this.op = signal('')
|
||||||
this.treeNative = new TreeNative()
|
|
||||||
|
|
||||||
window._sync = new Sync()
|
window._sync = new Sync()
|
||||||
window._sync.run()
|
window._sync.run()
|
||||||
|
|
@ -78,7 +76,6 @@ export class Notes2 extends Component {
|
||||||
this.nodeUI.current.setNode(node)
|
this.nodeUI.current.setNode(node)
|
||||||
this.nodeUI.current.setCrumbs(ancestors)
|
this.nodeUI.current.setCrumbs(ancestors)
|
||||||
this.tree.setSelected(node, dontExpand)
|
this.tree.setSelected(node, dontExpand)
|
||||||
this.treeNative.setSelected(node, dontExpand)
|
|
||||||
}//}}}
|
}//}}}
|
||||||
logout() {//{{{
|
logout() {//{{{
|
||||||
localStorage.removeItem('session.UUID')
|
localStorage.removeItem('session.UUID')
|
||||||
|
|
@ -110,7 +107,6 @@ class Tree extends Component {
|
||||||
this.treeNodeComponents[node.UUID] = createRef()
|
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`<${TreeNode} key=${`treenode_${node.UUID}`} tree=${this} node=${node} ref=${this.treeNodeComponents[node.UUID]} selected=${node.UUID === app.state.startNode?.UUID} />`
|
||||||
})
|
})
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div id="tree" ref=${this.treeDiv} tabindex="0">
|
<div id="tree" ref=${this.treeDiv} tabindex="0">
|
||||||
<div id="logo" onclick=${() => _notes2.current.goToNode(ROOT_NODE)}><img src="/images/${_VERSION}/logo.svg" /></div>
|
<div id="logo" onclick=${() => _notes2.current.goToNode(ROOT_NODE)}><img src="/images/${_VERSION}/logo.svg" /></div>
|
||||||
|
|
@ -122,7 +118,7 @@ class Tree extends Component {
|
||||||
</div>`
|
</div>`
|
||||||
}//}}}
|
}//}}}
|
||||||
componentDidMount() {//{{{
|
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.
|
// This will show and select the treenode that is selected in the node UI.
|
||||||
const node = _notes2.current?.nodeUI.current?.node.value
|
const node = _notes2.current?.nodeUI.current?.node.value
|
||||||
|
|
@ -542,13 +538,13 @@ class OpSearch extends Op {
|
||||||
for (const r of results) {
|
for (const r of results) {
|
||||||
const ancestors = r.ancestry.reverse().map(a => {
|
const ancestors = r.ancestry.reverse().map(a => {
|
||||||
const div = tmpl(`<div class="ancestor">${a.data.Name}</div>`)
|
const div = tmpl(`<div class="ancestor">${a.data.Name}</div>`)
|
||||||
div[0].addEventListener('click', () => _notes2.current.goToNode(a.UUID))
|
div[0].addEventListener('click', ()=>_notes2.current.goToNode(a.UUID))
|
||||||
return div[0]
|
return div[0]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const div = tmpl(`<div>${r.name}</div>`)
|
const div = tmpl(`<div>${r.name}</div>`)
|
||||||
div[0].addEventListener('click', () => _notes2.current.goToNode(r.uuid))
|
div[0].addEventListener('click', ()=>_notes2.current.goToNode(r.uuid))
|
||||||
rs.push(...div)
|
rs.push(...div)
|
||||||
|
|
||||||
const ancDev = tmpl('<div class="ancestors"></div>')
|
const ancDev = tmpl('<div class="ancestors"></div>')
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,31 @@
|
||||||
import { API } from 'api'
|
import { API } from 'api'
|
||||||
import { Node } from 'node'
|
import { Node } from 'node'
|
||||||
import { CustomHTMLElement } from './lib/custom_html_element.mjs'
|
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 {
|
export class Sync {
|
||||||
constructor() {//{{{
|
constructor() {//{{{
|
||||||
this.listeners = []
|
this.listeners = []
|
||||||
this.messagesReceived = []
|
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() {//{{{
|
async run() {//{{{
|
||||||
try {
|
try {
|
||||||
|
|
@ -19,8 +38,8 @@ export class Sync {
|
||||||
|
|
||||||
let nodeCount = await this.getNodeCount(oldMax)
|
let nodeCount = await this.getNodeCount(oldMax)
|
||||||
nodeCount += await nodeStore.sendQueue.count()
|
nodeCount += await nodeStore.sendQueue.count()
|
||||||
|
const msg = { op: SYNC_COUNT, count: nodeCount }
|
||||||
_mbus.dispatch('SYNC_COUNT', { count: nodeCount })
|
this.pushMessage(msg)
|
||||||
|
|
||||||
await this.nodesFromServer(oldMax)
|
await this.nodesFromServer(oldMax)
|
||||||
.then(durationNodes => {
|
.then(durationNodes => {
|
||||||
|
|
@ -30,7 +49,7 @@ export class Sync {
|
||||||
|
|
||||||
await this.nodesToServer()
|
await this.nodesToServer()
|
||||||
} finally {
|
} finally {
|
||||||
_mbus.dispatch('SYNC_DONE')
|
this.pushMessage({ op: SYNC_DONE })
|
||||||
}
|
}
|
||||||
}//}}}
|
}//}}}
|
||||||
async getNodeCount(oldMax) {//{{{
|
async getNodeCount(oldMax) {//{{{
|
||||||
|
|
@ -41,7 +60,6 @@ export class Sync {
|
||||||
async nodesFromServer(oldMax) {//{{{
|
async nodesFromServer(oldMax) {//{{{
|
||||||
const syncStart = Date.now()
|
const syncStart = Date.now()
|
||||||
let syncEnd
|
let syncEnd
|
||||||
let handled = 0
|
|
||||||
try {
|
try {
|
||||||
let currMax = oldMax
|
let currMax = oldMax
|
||||||
let offset = 0
|
let offset = 0
|
||||||
|
|
@ -65,24 +83,12 @@ export class Sync {
|
||||||
* sync be preserved in the backend. */
|
* sync be preserved in the backend. */
|
||||||
|
|
||||||
let backendNode = null
|
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) {
|
for (const i in res.Nodes) {
|
||||||
backendNode = new Node(res.Nodes[i], -1)
|
backendNode = new Node(res.Nodes[i], -1)
|
||||||
await this.handleNode(backendNode, objstore)
|
await window._sync.handleNode(backendNode)
|
||||||
|
|
||||||
handled++
|
|
||||||
if (handled % 100 === 0)
|
|
||||||
_mbus.dispatch('SYNC_HANDLED', { handled })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} while (res.Continue)
|
} while (res.Continue)
|
||||||
_mbus.dispatch('SYNC_HANDLED', { handled })
|
|
||||||
|
|
||||||
nodeStore.setAppState('latest_sync_node', currMax)
|
nodeStore.setAppState('latest_sync_node', currMax)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -95,16 +101,16 @@ export class Sync {
|
||||||
}
|
}
|
||||||
return (syncEnd - syncStart)
|
return (syncEnd - syncStart)
|
||||||
}//}}}
|
}//}}}
|
||||||
async handleNode(backendNode, objstore) {//{{{
|
async handleNode(backendNode) {//{{{
|
||||||
try {
|
try {
|
||||||
/* Retrieving the local copy of this node from IndexedDB.
|
/* Retrieving the local copy of this node from IndexedDB.
|
||||||
* The backend node can be discarded if it is older than
|
* The backend node can be discarded if it is older than
|
||||||
* the local copy since it is considered history preserved
|
* the local copy since it is considered history preserved
|
||||||
* in the backend. */
|
* in the backend. */
|
||||||
return nodeStore.get(backendNode.UUID, objstore)
|
return nodeStore.get(backendNode.UUID)
|
||||||
.then(localNode => {
|
.then(async localNode => {
|
||||||
if (localNode.updated() >= backendNode.updated()) {
|
if (localNode.updated() >= backendNode.updated()) {
|
||||||
console.debug(`History from backend: ${backendNode.UUID}`)
|
console.log(`History from backend: ${backendNode.UUID}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,17 +120,17 @@ export class Sync {
|
||||||
*
|
*
|
||||||
* If the local node has seen change, the change is already
|
* If the local node has seen change, the change is already
|
||||||
* placed into the send_queue anyway. */
|
* placed into the send_queue anyway. */
|
||||||
return nodeStore.add([backendNode], objstore)
|
return nodeStore.add([backendNode])
|
||||||
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(async () => {
|
||||||
// Not found in IndexedDB - OK to just insert since it only exists in backend.
|
// Not found in IndexedDB - OK to just insert since it only exists in backend.
|
||||||
return nodeStore.add([backendNode], objstore)
|
return nodeStore.add([backendNode])
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
} finally {
|
} finally {
|
||||||
//_mbus.dispatch('SYNC_HANDLED', { count: 1 })
|
this.pushMessage({ op: SYNC_HANDLED, count: 1 })
|
||||||
}
|
}
|
||||||
}//}}}
|
}//}}}
|
||||||
async nodesToServer() {//{{{
|
async nodesToServer() {//{{{
|
||||||
|
|
@ -143,7 +149,7 @@ export class Sync {
|
||||||
const res = await API.query('POST', '/sync/to_server', request)
|
const res = await API.query('POST', '/sync/to_server', request)
|
||||||
if (!res.OK) {
|
if (!res.OK) {
|
||||||
// TODO - implement better error management here.
|
// TODO - implement better error management here.
|
||||||
console.error(res)
|
console.log(res)
|
||||||
alert(res)
|
alert(res)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -151,7 +157,7 @@ export class Sync {
|
||||||
// Nodes are archived on server and can now be deleted from the send queue.
|
// Nodes are archived on server and can now be deleted from the send queue.
|
||||||
const keys = nodesToSend.map(node => node.ClientSequence)
|
const keys = nodesToSend.map(node => node.ClientSequence)
|
||||||
await nodeStore.sendQueue.delete(keys)
|
await nodeStore.sendQueue.delete(keys)
|
||||||
_mbus.dispatch('SYNC_UPLOADED', { count: nodesToSend.length })
|
this.pushMessage({ op: SYNC_HANDLED, count: nodesToSend.length })
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.trace(e)
|
console.trace(e)
|
||||||
|
|
@ -162,66 +168,77 @@ export class Sync {
|
||||||
}//}}}
|
}//}}}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class N2SyncProgress extends CustomHTMLElement {
|
export class SyncProgress extends Component {
|
||||||
static {
|
|
||||||
this.tmpl = document.createElement('template')
|
|
||||||
this.tmpl.innerHTML = `
|
|
||||||
<progress data-el="progress" min=0 max=137 value=0></progress>
|
|
||||||
<div data-el="count" class="count">0 / 0</div>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
constructor() {//{{{
|
constructor() {//{{{
|
||||||
super()
|
super()
|
||||||
|
|
||||||
this.reset()
|
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() {//{{{
|
reset() {//{{{
|
||||||
|
this.forceUpdateRequest = null
|
||||||
this.state = {
|
this.state = {
|
||||||
nodesToSync: 0,
|
nodesToSync: 0,
|
||||||
nodesSynced: 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(event) {//{{{
|
progressHandler(msg) {//{{{
|
||||||
const eventData = event.detail.data
|
switch (msg.op) {
|
||||||
switch (event.type) {
|
case SYNC_COUNT:
|
||||||
case 'SYNC_COUNT':
|
this.setState({ nodesToSync: msg.count })
|
||||||
this.state.nodesToSync = eventData.count
|
|
||||||
this.setSyncState(true)
|
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'SYNC_HANDLED':
|
case SYNC_HANDLED:
|
||||||
this.state.nodesSynced = eventData.handled
|
this.state.nodesSynced += msg.count
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'SYNC_DONE':
|
case SYNC_DONE:
|
||||||
// Hides the progress bar.
|
// Hides the progress bar.
|
||||||
this.setSyncState(false)
|
this.setState({ syncedDone: true })
|
||||||
|
|
||||||
// Don't update anything if nothing was synced.
|
// Don't update anything if nothing was synced.
|
||||||
if (this.state.nodesSynced === 0)
|
if (this.state.nodesSynced === 0)
|
||||||
break
|
break
|
||||||
|
|
||||||
// Reload the tree nodes to reflect the new/updated nodes.
|
// Reload the tree nodes to reflect the new/updated nodes.
|
||||||
window._app.tree.reset()
|
if (window._notes2?.current?.reloadTree.value !== null) {
|
||||||
|
nodeStore.purgeCache()
|
||||||
|
window._notes2.current.reloadTree.value = window._notes2.current.reloadTree.value + 1
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
this.render()
|
|
||||||
}//}}}
|
}//}}}
|
||||||
render() {//{{{
|
render(_, { nodesToSync, nodesSynced }) {//{{{
|
||||||
this.elProgress.max = this.state.nodesToSync
|
if (nodesToSync === 0)
|
||||||
this.elProgress.value = this.state.nodesSynced
|
return html`<div id="sync-progress"></div>`
|
||||||
this.elCount.innerText = `${this.state.nodesSynced} / ${this.state.nodesToSync}`
|
|
||||||
|
return html`
|
||||||
|
<div id="sync-progress">
|
||||||
|
<progress min=0 max=${nodesToSync} value=${nodesSynced}></progress>
|
||||||
|
<div class="count">${nodesSynced} / ${nodesToSync}</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
}//}}}
|
}//}}}
|
||||||
setSyncState(state) {// {{{
|
|
||||||
if (state)
|
|
||||||
this.classList.add('show')
|
|
||||||
else
|
|
||||||
setTimeout(() => this.classList.remove('show'), 1500)
|
|
||||||
}// }}}
|
|
||||||
}
|
}
|
||||||
customElements.define('n2-syncprogress', N2SyncProgress)
|
|
||||||
|
|
||||||
// vim: foldmethod=marker
|
// vim: foldmethod=marker
|
||||||
|
|
|
||||||
|
|
@ -1,451 +0,0 @@
|
||||||
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 = `
|
|
||||||
<div data-el="logo" id="logo"><img src="/images/${_VERSION}/logo.svg" /></div>
|
|
||||||
<div class="icons">
|
|
||||||
<img data-el="search" class='search' src="/images/${_VERSION}/icon_search.svg" style="height: 22px" />
|
|
||||||
<img data-el="sync" class='sync' src="/images/${_VERSION}/icon_refresh.svg" />
|
|
||||||
</div>
|
|
||||||
<div data-el="treenodes"></div>
|
|
||||||
`
|
|
||||||
}// }}}
|
|
||||||
|
|
||||||
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 = `
|
|
||||||
<div data-el="expand-toggle" class="expand-toggle">
|
|
||||||
<img data-el="expand">
|
|
||||||
</div>
|
|
||||||
<div data-el="name" class="name"></div>
|
|
||||||
<div data-el="children" class="children"></div>
|
|
||||||
`
|
|
||||||
}// }}}
|
|
||||||
|
|
||||||
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 <img> 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
|
|
||||||
350
static/less/notes2.less
Normal file
350
static/less/notes2.less
Normal file
|
|
@ -0,0 +1,350 @@
|
||||||
|
@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: "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
:root {
|
@color1: #fe5f55;
|
||||||
--color1: #fe5f55;
|
@color2: #efede8;
|
||||||
--color2: #efede8;
|
@color3: #666;
|
||||||
--color3: #666;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,13 @@ const CACHED_ASSETS = [
|
||||||
'/css/{{ .VERSION }}/main.css',
|
'/css/{{ .VERSION }}/main.css',
|
||||||
'/css/{{ .VERSION }}/notes2.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/fullcalendar.min.js',
|
||||||
'/js/{{ .VERSION }}/lib/node_modules/marked/marked.min.js',
|
'/js/{{ .VERSION }}/lib/node_modules/marked/marked.min.js',
|
||||||
'/js/{{ .VERSION }}/lib/sjcl.js',
|
'/js/{{ .VERSION }}/lib/sjcl.js',
|
||||||
|
|
|
||||||
|
|
@ -2,26 +2,8 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
|
<meta name="viewport" content="initial-scale=1.0, user-scalable=yes" />
|
||||||
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/main.css">
|
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/main.css">
|
||||||
<script type="importmap">
|
|
||||||
{
|
|
||||||
"imports": {
|
|
||||||
"api": "/js/{{ .VERSION }}/api.mjs",
|
|
||||||
"sync": "/js/{{ .VERSION }}/sync.mjs",
|
|
||||||
"key": "/js/{{ .VERSION }}/key.mjs",
|
|
||||||
"checklist": "/js/{{ .VERSION }}/checklist.mjs",
|
|
||||||
"crypto": "/js/{{ .VERSION }}/crypto.mjs",
|
|
||||||
"node_store": "/js/{{ .VERSION }}/node_store.mjs",
|
|
||||||
"node": "/js/{{ .VERSION }}/node.mjs",
|
|
||||||
"tree": "/js/{{ .VERSION }}/tree.mjs"
|
|
||||||
{{/*
|
|
||||||
"session": "/js/{{ .VERSION }}/session.mjs",
|
|
||||||
"ws": "/_js/{{ .VERSION }}/websocket.mjs"
|
|
||||||
*/}}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<script>
|
<script>
|
||||||
window._VERSION = "{{ .VERSION }}"
|
window._VERSION = "{{ .VERSION }}"
|
||||||
|
|
||||||
|
|
@ -32,6 +14,33 @@
|
||||||
import { MessageBus } from '/js/{{ .VERSION }}/mbus.mjs'
|
import { MessageBus } from '/js/{{ .VERSION }}/mbus.mjs'
|
||||||
window._mbus = new MessageBus()
|
window._mbus = new MessageBus()
|
||||||
</script>
|
</script>
|
||||||
|
<script type="importmap">
|
||||||
|
{
|
||||||
|
"imports": {
|
||||||
|
"preact": "/js/{{ .VERSION }}/lib/preact/preact.mjs",
|
||||||
|
"preact/hooks": "/js/{{ .VERSION }}/lib/preact/hooks.mjs",
|
||||||
|
{{- if .Data._dev }}
|
||||||
|
"preact/debug": "/js/{{ .VERSION }}/lib/preact/debug.mjs",
|
||||||
|
"preact/devtools": "/js/{{ .VERSION }}/lib/preact/devtools.mjs",
|
||||||
|
{{- end }}
|
||||||
|
"@preact/signals-core": "/js/{{ .VERSION }}/lib/signals/signals-core.mjs",
|
||||||
|
"preact/signals": "/js/{{ .VERSION }}/lib/signals/signals.mjs",
|
||||||
|
"htm": "/js/{{ .VERSION }}/lib/htm/htm.mjs",
|
||||||
|
|
||||||
|
"api": "/js/{{ .VERSION }}/api.mjs",
|
||||||
|
"sync": "/js/{{ .VERSION }}/sync.mjs",
|
||||||
|
"key": "/js/{{ .VERSION }}/key.mjs",
|
||||||
|
"checklist": "/js/{{ .VERSION }}/checklist.mjs",
|
||||||
|
"crypto": "/js/{{ .VERSION }}/crypto.mjs",
|
||||||
|
"node_store": "/js/{{ .VERSION }}/node_store.mjs",
|
||||||
|
"node": "/js/{{ .VERSION }}/node.mjs"
|
||||||
|
{{/*
|
||||||
|
"session": "/js/{{ .VERSION }}/session.mjs",
|
||||||
|
"ws": "/_js/{{ .VERSION }}/websocket.mjs"
|
||||||
|
*/}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<script type="text/javascript" src="/js/{{ .VERSION }}/lib/sjcl.js"></script>
|
<script type="text/javascript" src="/js/{{ .VERSION }}/lib/sjcl.js"></script>
|
||||||
<script type="text/javascript" src="/js/{{ .VERSION }}/lib/node_modules/marked/marked.min.js"></script>
|
<script type="text/javascript" src="/js/{{ .VERSION }}/lib/node_modules/marked/marked.min.js"></script>
|
||||||
<script type="text/javascript" src="/js/{{ .VERSION }}/lib/fullcalendar.min.js"></script>
|
<script type="text/javascript" src="/js/{{ .VERSION }}/lib/fullcalendar.min.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,35 @@
|
||||||
{{ define "page" }}
|
{{ define "page" }}
|
||||||
<div id="notes2">
|
<div id="notes2"></div>
|
||||||
<div id="tree" tabindex=0></div>
|
|
||||||
<div id="crumbs"></div>
|
|
||||||
<n2-syncprogress></n2-syncprogress>
|
|
||||||
<n2-nodeui id="note"></n2-nodeui>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/notes2.css">
|
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/notes2.css">
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import {NodeStore} from 'node_store'
|
import { h, Component, render, createRef } from 'preact'
|
||||||
import {App} from "/js/{{ .VERSION }}/app.mjs"
|
import htm from 'htm'
|
||||||
import {API} from 'api'
|
{{ if .Data._dev -}}
|
||||||
import {Sync} from 'sync'
|
import 'preact/debug'
|
||||||
|
import 'preact/devtools'
|
||||||
|
{{- end }}
|
||||||
|
import { NodeStore } from 'node_store'
|
||||||
|
import { Notes2 } from "/js/{{ .VERSION }}/notes2.mjs"
|
||||||
|
import { API } from 'api'
|
||||||
|
import { Sync } from 'sync'
|
||||||
|
|
||||||
window.Sync = Sync
|
window.Sync = Sync
|
||||||
|
|
||||||
if (!API.hasAuthenticationToken()) {
|
if (!API.hasAuthenticationToken()) {
|
||||||
location.href = '/login'
|
location.href = '/login'
|
||||||
} else {
|
} else {
|
||||||
try {
|
const html = htm.bind(h)
|
||||||
window.nodeStore = new NodeStore()
|
try {
|
||||||
window.nodeStore.initializeDB().then(() => {
|
window.nodeStore = new NodeStore()
|
||||||
window._app = new App()
|
window.nodeStore.initializeDB().then(() => {
|
||||||
})
|
window._notes2 = createRef()
|
||||||
} catch (e) {
|
render(html`<${Notes2} ref=${window._notes2} />`, document.getElementById('notes2'))
|
||||||
alert(e)
|
})
|
||||||
}
|
} catch (e) {
|
||||||
|
alert(e)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue