wip
This commit is contained in:
parent
c255b58335
commit
1812873e33
@ -5,6 +5,7 @@ import (
|
|||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
// Standard
|
// Standard
|
||||||
|
"errors"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -33,5 +34,13 @@ func ConfigRead(filename string) (config Config, err error) {
|
|||||||
if err != nil { return }
|
if err != nil { return }
|
||||||
|
|
||||||
err = yaml.Unmarshal(rawConfigData, &config)
|
err = yaml.Unmarshal(rawConfigData, &config)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Session.DaysValid == 0 {
|
||||||
|
err = errors.New("Configuration: session.daysvalid needs to be higher than 0.")
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
62
main.go
62
main.go
@ -14,7 +14,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const VERSION = "v0.0.3";
|
const VERSION = "v0.0.5";
|
||||||
const LISTEN_HOST = "0.0.0.0";
|
const LISTEN_HOST = "0.0.0.0";
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -59,6 +59,8 @@ func main() {// {{{
|
|||||||
http.HandleFunc("/session/retrieve", sessionRetrieve)
|
http.HandleFunc("/session/retrieve", sessionRetrieve)
|
||||||
http.HandleFunc("/session/authenticate", sessionAuthenticate)
|
http.HandleFunc("/session/authenticate", sessionAuthenticate)
|
||||||
http.HandleFunc("/node/retrieve", nodeRetrieve)
|
http.HandleFunc("/node/retrieve", nodeRetrieve)
|
||||||
|
http.HandleFunc("/node/create", nodeCreate)
|
||||||
|
http.HandleFunc("/node/update", nodeUpdate)
|
||||||
http.HandleFunc("/ws", websocketHandler)
|
http.HandleFunc("/ws", websocketHandler)
|
||||||
http.HandleFunc("/", staticHandler)
|
http.HandleFunc("/", staticHandler)
|
||||||
|
|
||||||
@ -202,6 +204,64 @@ func nodeRetrieve(w http.ResponseWriter, r *http.Request) {// {{{
|
|||||||
"Node": node,
|
"Node": node,
|
||||||
})
|
})
|
||||||
}// }}}
|
}// }}}
|
||||||
|
func nodeCreate(w http.ResponseWriter, r *http.Request) {// {{{
|
||||||
|
log.Println("/node/create")
|
||||||
|
var err error
|
||||||
|
var session Session
|
||||||
|
|
||||||
|
if session, _, err = ValidateSession(r, true); err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req := struct {
|
||||||
|
Name string
|
||||||
|
ParentID int
|
||||||
|
}{}
|
||||||
|
if err = parseRequest(r, &req); err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
node, err := session.CreateNode(req.ParentID, req.Name)
|
||||||
|
if err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData(w, map[string]interface{}{
|
||||||
|
"OK": true,
|
||||||
|
"Node": node,
|
||||||
|
})
|
||||||
|
}// }}}
|
||||||
|
func nodeUpdate(w http.ResponseWriter, r *http.Request) {// {{{
|
||||||
|
var err error
|
||||||
|
var session Session
|
||||||
|
|
||||||
|
if session, _, err = ValidateSession(r, true); err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req := struct {
|
||||||
|
NodeID int
|
||||||
|
Content string
|
||||||
|
}{}
|
||||||
|
if err = parseRequest(r, &req); err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = session.UpdateNode(req.NodeID, req.Content)
|
||||||
|
if err != nil {
|
||||||
|
responseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData(w, map[string]interface{}{
|
||||||
|
"OK": true,
|
||||||
|
})
|
||||||
|
}// }}}
|
||||||
|
|
||||||
func newTemplate(requestPath string) (tmpl *template.Template, err error) {// {{{
|
func newTemplate(requestPath string) (tmpl *template.Template, err error) {// {{{
|
||||||
// Append index.html if needed for further reading of the file
|
// Append index.html if needed for further reading of the file
|
||||||
|
48
node.go
48
node.go
@ -110,6 +110,8 @@ func (session Session) Node(nodeID int) (node Node, err error) {// {{{
|
|||||||
Level int
|
Level int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
node = Node{}
|
||||||
|
node.Children = []Node{}
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
row := resultRow{}
|
row := resultRow{}
|
||||||
if err = rows.StructScan(&row); err != nil {
|
if err = rows.StructScan(&row); err != nil {
|
||||||
@ -117,8 +119,6 @@ func (session Session) Node(nodeID int) (node Node, err error) {// {{{
|
|||||||
}
|
}
|
||||||
|
|
||||||
if row.Level == 0 {
|
if row.Level == 0 {
|
||||||
node = Node{}
|
|
||||||
node.Children = []Node{}
|
|
||||||
node.ID = row.ID
|
node.ID = row.ID
|
||||||
node.UserID = row.UserID
|
node.UserID = row.UserID
|
||||||
node.ParentID = row.ParentID
|
node.ParentID = row.ParentID
|
||||||
@ -179,5 +179,49 @@ func (session Session) NodeCrumbs(nodeID int) (nodes []Node, err error) {// {{{
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}// }}}
|
}// }}}
|
||||||
|
func (session Session) CreateNode(parentID int, name string) (node Node, err error) {// {{{
|
||||||
|
var rows *sqlx.Rows
|
||||||
|
|
||||||
|
rows, err = db.Queryx(`
|
||||||
|
INSERT INTO node(user_id, parent_id, name)
|
||||||
|
VALUES($1, NULLIF($2, 0)::integer, $3)
|
||||||
|
RETURNING
|
||||||
|
id,
|
||||||
|
user_id,
|
||||||
|
COALESCE(parent_id, 0) AS parent_id,
|
||||||
|
name,
|
||||||
|
content
|
||||||
|
`,
|
||||||
|
session.UserID,
|
||||||
|
parentID,
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
node = Node{}
|
||||||
|
if err = rows.StructScan(&node); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
node.Children = []Node{}
|
||||||
|
node.Complete = true
|
||||||
|
}
|
||||||
|
|
||||||
|
node.Crumbs, err = session.NodeCrumbs(node.ID)
|
||||||
|
return
|
||||||
|
}// }}}
|
||||||
|
func (session Session) UpdateNode(nodeID int, content string) (err error) {// {{{
|
||||||
|
_, err = db.Exec(`
|
||||||
|
UPDATE node SET content = $1 WHERE user_id = $2 AND id = $3
|
||||||
|
`,
|
||||||
|
content,
|
||||||
|
session.UserID,
|
||||||
|
nodeID,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}// }}}
|
||||||
|
|
||||||
// vim: foldmethod=marker
|
// vim: foldmethod=marker
|
||||||
|
17
sql/0002.sql
Normal file
17
sql/0002.sql
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
ALTER TABLE node ADD COLUMN updated TIMESTAMP NOT NULL DEFAULT NOW();
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION node_update_timestamp()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE PLPGSQL
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
IF NEW.updated = OLD.updated THEN
|
||||||
|
UPDATE node SET updated = NOW() WHERE id=NEW.id;
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE TRIGGER node_update AFTER UPDATE ON node
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE PROCEDURE node_update_timestamp()
|
@ -11,7 +11,7 @@
|
|||||||
border-bottom: 1px solid #fff;
|
border-bottom: 1px solid #fff;
|
||||||
font-size: 18pt;
|
font-size: 18pt;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #494949;
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
#login input:focus {
|
#login input:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
@ -19,7 +19,7 @@
|
|||||||
#login button {
|
#login button {
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
border: 1px solid #666;
|
border: 1px solid #666;
|
||||||
background: #494949;
|
background: #fff;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
padding: 16px 32px;
|
padding: 16px 32px;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
|
@ -1,34 +1,68 @@
|
|||||||
|
html {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
*,
|
||||||
|
*:before,
|
||||||
|
*:after {
|
||||||
|
box-sizing: inherit;
|
||||||
|
}
|
||||||
|
[onClick] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
font-family: 'Liberation Mono', monospace;
|
font-family: 'Liberation Mono', monospace;
|
||||||
font-size: 14pt;
|
font-size: 14pt;
|
||||||
background-color: #494949;
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
color: #ecbf00;
|
color: #ecbf00;
|
||||||
}
|
}
|
||||||
#app {
|
#app {
|
||||||
|
display: grid;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr min-content;
|
||||||
|
align-items: center;
|
||||||
|
background: #ecbf00;
|
||||||
|
padding: 0px;
|
||||||
|
color: #3a2f00;
|
||||||
|
}
|
||||||
|
header.modified {
|
||||||
|
background: #c84a37;
|
||||||
|
color: #faedeb;
|
||||||
|
}
|
||||||
|
header .name {
|
||||||
|
font-weight: bold;
|
||||||
|
padding-left: 16px;
|
||||||
|
}
|
||||||
|
header .add {
|
||||||
|
font-size: 2em;
|
||||||
|
padding-right: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
.crumbs {
|
.crumbs {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
padding: 8px 16px;
|
padding: 16px;
|
||||||
background: #ecbf00;
|
background: #333;
|
||||||
color: #000;
|
color: #fff;
|
||||||
box-shadow: 0px 2px 8px 0px rgba(0, 0, 0, 0.4);
|
|
||||||
}
|
}
|
||||||
.crumbs .crumb {
|
.crumbs .crumb {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
.crumbs .crumb:after {
|
.crumbs .crumb:after {
|
||||||
content: ">";
|
content: "•";
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
color: #a08100;
|
color: #ecbf00;
|
||||||
}
|
}
|
||||||
.crumbs .crumb:last-child {
|
.crumbs .crumb:last-child {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
@ -41,22 +75,41 @@ h1 {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
padding: 16px 16px 0px 16px;
|
padding: 16px 16px 0px 16px;
|
||||||
background-color: #3c3c3c;
|
background-color: #505050;
|
||||||
box-shadow: 0px 0px 16px -4px rgba(0, 0, 0, 0.55) inset;
|
|
||||||
}
|
}
|
||||||
.child-nodes .child-node {
|
.child-nodes .child-node {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: #292929;
|
background-color: #2f2f2f;
|
||||||
margin-right: 16px;
|
margin-right: 12px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
.node-name {
|
.node-name {
|
||||||
padding: 16px;
|
padding: 32px;
|
||||||
|
background: #fff;
|
||||||
|
color: #000;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.node-content {
|
.node-content {
|
||||||
padding: 32px;
|
justify-self: center;
|
||||||
|
padding: 0px 32px 32px 32px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: #333;
|
||||||
|
width: 100ex;
|
||||||
|
}
|
||||||
|
.node-content[contenteditable] {
|
||||||
|
outline: 0px solid transparent;
|
||||||
|
}
|
||||||
|
@media only screen and (max-width: 100ex) {
|
||||||
|
.node-content {
|
||||||
|
width: initial;
|
||||||
|
justify-self: start;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ const html = htm.bind(h)
|
|||||||
class App extends Component {
|
class App extends Component {
|
||||||
constructor() {//{{{
|
constructor() {//{{{
|
||||||
super()
|
super()
|
||||||
|
this.websocket_uri = `wss://notes.ahall.se/ws`
|
||||||
this.websocket = null
|
this.websocket = null
|
||||||
this.websocket_int_ping = null
|
this.websocket_int_ping = null
|
||||||
this.websocket_int_reconnect = null
|
this.websocket_int_reconnect = null
|
||||||
@ -19,6 +20,7 @@ class App extends Component {
|
|||||||
this.session.initialize()
|
this.session.initialize()
|
||||||
this.login = createRef()
|
this.login = createRef()
|
||||||
this.nodeUI = createRef()
|
this.nodeUI = createRef()
|
||||||
|
this.nodeModified = signal(false)
|
||||||
}//}}}
|
}//}}}
|
||||||
render() {//{{{
|
render() {//{{{
|
||||||
if(!this.session.initialized) {
|
if(!this.session.initialized) {
|
||||||
@ -41,7 +43,7 @@ class App extends Component {
|
|||||||
}, 1000)
|
}, 1000)
|
||||||
}//}}}
|
}//}}}
|
||||||
wsConnect() {//{{{
|
wsConnect() {//{{{
|
||||||
this.websocket = new WebSocket(`ws://192.168.11.60:1371/ws`)
|
this.websocket = new WebSocket(this.websocket_uri)
|
||||||
this.websocket.onopen = evt=>this.wsOpen(evt)
|
this.websocket.onopen = evt=>this.wsOpen(evt)
|
||||||
this.websocket.onmessage = evt=>this.wsMessage(evt)
|
this.websocket.onmessage = evt=>this.wsMessage(evt)
|
||||||
this.websocket.onerror = evt=>this.wsError(evt)
|
this.websocket.onerror = evt=>this.wsError(evt)
|
||||||
@ -164,28 +166,6 @@ class Login extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Init{{{
|
// Init{{{
|
||||||
//let urlParams = new URLSearchParams(window.location.search)
|
|
||||||
|
|
||||||
/*
|
|
||||||
async function debug(type, context, data) {
|
|
||||||
await fetch("https://msg.kon-it.se/log", {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
systemId: 12,
|
|
||||||
type,
|
|
||||||
context,
|
|
||||||
data: JSON.stringify(data, null, 4),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
window.onerror = (event, source, lineno, colon, error) => {
|
|
||||||
debug('Notes', 'error', {
|
|
||||||
event, source, lineno, colon, error
|
|
||||||
})
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
window._app = createRef()
|
window._app = createRef()
|
||||||
window._resourceModels = []
|
window._resourceModels = []
|
||||||
render(html`<${App} ref=${window._app} />`, document.getElementById('app'))
|
render(html`<${App} ref=${window._app} />`, document.getElementById('app'))
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { h, Component } from 'preact'
|
import { h, Component, createRef } from 'preact'
|
||||||
import htm from 'htm'
|
import htm from 'htm'
|
||||||
import { signal } from 'preact/signals'
|
import { signal } from 'preact/signals'
|
||||||
const html = htm.bind(h)
|
const html = htm.bind(h)
|
||||||
@ -7,6 +7,7 @@ export class NodeUI extends Component {
|
|||||||
constructor() {//{{{
|
constructor() {//{{{
|
||||||
super()
|
super()
|
||||||
this.node = signal(null)
|
this.node = signal(null)
|
||||||
|
this.nodeContent = createRef()
|
||||||
window.addEventListener('popstate', evt=>{
|
window.addEventListener('popstate', evt=>{
|
||||||
if(evt.state && evt.state.hasOwnProperty('nodeID'))
|
if(evt.state && evt.state.hasOwnProperty('nodeID'))
|
||||||
this.goToNode(evt.state.nodeID, true)
|
this.goToNode(evt.state.nodeID, true)
|
||||||
@ -24,7 +25,7 @@ export class NodeUI extends Component {
|
|||||||
html`<div class="crumb" onclick=${()=>this.goToNode(0)}>Start</div>`
|
html`<div class="crumb" onclick=${()=>this.goToNode(0)}>Start</div>`
|
||||||
]
|
]
|
||||||
|
|
||||||
crumbs = crumbs.concat(node.Crumbs.slice(1).map(node=>
|
crumbs = crumbs.concat(node.Crumbs.slice(0).map(node=>
|
||||||
html`<div class="crumb" onclick=${()=>this.goToNode(node.ID)}>${node.Name}</div>`
|
html`<div class="crumb" onclick=${()=>this.goToNode(node.ID)}>${node.Name}</div>`
|
||||||
).reverse())
|
).reverse())
|
||||||
|
|
||||||
@ -36,27 +37,83 @@ export class NodeUI extends Component {
|
|||||||
<div class="child-node" onclick=${()=>this.goToNode(child.ID)}>${child.Name}</div>
|
<div class="child-node" onclick=${()=>this.goToNode(child.ID)}>${child.Name}</div>
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
let modified = ''
|
||||||
|
if(this.props.app.nodeModified.value)
|
||||||
|
modified = 'modified';
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
${node.ID > 0 ? html`<div class="crumbs">${crumbs}</crumbs>` : html``}
|
<header class="${modified}" onclick=${()=>this.saveNode()}>
|
||||||
<div class="node-name">${node.Name}</div>
|
<div class="name">Notes</div>
|
||||||
|
<div class="add" onclick=${()=>this.createNode()}>+</div>
|
||||||
|
</header>
|
||||||
|
<div class="crumbs">${crumbs}</crumbs>
|
||||||
${children.length > 0 ? html`<div class="child-nodes">${children}</div>` : html``}
|
${children.length > 0 ? html`<div class="child-nodes">${children}</div>` : html``}
|
||||||
<div class="node-content">${node.Content}</div>
|
<div class="node-name">${node.Name}</div>
|
||||||
|
<${NodeContent} key=${node.ID} content=${node.Content} ref=${this.nodeContent} />
|
||||||
`
|
`
|
||||||
}//}}}
|
}//}}}
|
||||||
componentDidMount() {//{{{
|
componentDidMount() {//{{{
|
||||||
let root = new Node(this.props.app, 0)
|
let urlParams = new URLSearchParams(window.location.search)
|
||||||
|
let nodeID = urlParams.get('node')
|
||||||
|
let root = new Node(this.props.app, nodeID ? parseInt(nodeID) : 0)
|
||||||
|
|
||||||
root.retrieve(node=>{
|
root.retrieve(node=>{
|
||||||
this.node.value = node
|
this.node.value = node
|
||||||
})
|
})
|
||||||
}//}}}
|
}//}}}
|
||||||
goToNode(nodeID, dontPush) {//{{{
|
goToNode(nodeID, dontPush) {//{{{
|
||||||
|
if(this.props.app.nodeModified.value) {
|
||||||
|
if(!confirm("Changes not saved. Do you want to discard changes?"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if(!dontPush)
|
if(!dontPush)
|
||||||
history.pushState({ nodeID }, '', `#${nodeID}`)
|
history.pushState({ nodeID }, '', `/?node=${nodeID}`)
|
||||||
let node = new Node(this.props.app, nodeID)
|
let node = new Node(this.props.app, nodeID)
|
||||||
node.retrieve(node=>{
|
node.retrieve(node=>{
|
||||||
|
this.props.app.nodeModified.value = false
|
||||||
this.node.value = node
|
this.node.value = node
|
||||||
})
|
})
|
||||||
}//}}}
|
}//}}}
|
||||||
|
createNode() {//{{{
|
||||||
|
let name = prompt("Name")
|
||||||
|
if(!name)
|
||||||
|
return
|
||||||
|
|
||||||
|
this.props.app.request('/node/create', {
|
||||||
|
Name: name.trim(),
|
||||||
|
ParentID: this.node.value.ID,
|
||||||
|
})
|
||||||
|
.then(res=>{
|
||||||
|
this.goToNode(res.Node.ID)
|
||||||
|
})
|
||||||
|
.catch(this.props.app.responseError)
|
||||||
|
}//}}}
|
||||||
|
saveNode() {
|
||||||
|
let content = this.nodeContent.current.contentDiv.current.innerText
|
||||||
|
this.props.app.request('/node/update', {
|
||||||
|
NodeID: this.node.value.ID,
|
||||||
|
Content: content,
|
||||||
|
})
|
||||||
|
.then(res=>{
|
||||||
|
this.props.app.nodeModified.value = false
|
||||||
|
})
|
||||||
|
.catch(this.props.app.responseError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NodeContent extends Component {
|
||||||
|
constructor(props) {//{{{
|
||||||
|
super(props)
|
||||||
|
this.contentDiv = createRef()
|
||||||
|
this.state = {
|
||||||
|
modified: false,
|
||||||
|
//content: props.content,
|
||||||
|
}
|
||||||
|
}//}}}
|
||||||
|
render({ content }) {//{{{
|
||||||
|
return html`<div class="node-content" ref=${this.contentDiv} contenteditable=true oninput=${()=>window._app.current.nodeModified.value = true}>${content}</div>`
|
||||||
|
}//}}}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Node {
|
class Node {
|
||||||
|
@ -1,5 +1,17 @@
|
|||||||
@import "theme.less";
|
@import "theme.less";
|
||||||
|
|
||||||
|
html {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
*, *:before, *:after {
|
||||||
|
box-sizing: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
[onClick] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
@ -14,27 +26,54 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
|
display: grid;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr min-content;
|
||||||
|
align-items: center;
|
||||||
|
background: @accent_1;
|
||||||
|
padding: 0px;
|
||||||
|
color: darken(@accent_1, 35%);
|
||||||
|
|
||||||
|
&.modified {
|
||||||
|
background: @accent_3;
|
||||||
|
color: lighten(@accent_3, 45%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-weight: bold;
|
||||||
|
padding-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add {
|
||||||
|
font-size: 2em;
|
||||||
|
padding-right: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.crumbs {
|
.crumbs {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
padding: 8px 16px;
|
padding: 16px;
|
||||||
background: @accent_1;
|
background: #333;
|
||||||
color: #000;
|
color: #fff;
|
||||||
box-shadow: 0px 2px 8px 0px rgba(0,0,0,0.4);
|
|
||||||
|
|
||||||
.crumb {
|
.crumb {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.crumb:after {
|
.crumb:after {
|
||||||
content: ">";
|
content: "•";
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
color: darken(@accent_1, 15%);
|
color: @accent_1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.crumb:last-child {
|
.crumb:last-child {
|
||||||
@ -52,25 +91,46 @@ h1 {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
padding: 16px 16px 0px 16px;
|
padding: 16px 16px 0px 16px;
|
||||||
background-color: darken(@background, 5%);
|
background-color: #505050;
|
||||||
box-shadow: 0px 0px 16px -4px rgba(0,0,0,0.55) inset;
|
|
||||||
|
|
||||||
.child-node {
|
.child-node {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: darken(@background, 12.5%);
|
background-color: #2f2f2f;
|
||||||
margin-right: 16px;
|
margin-right: 12px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-name {
|
.node-name {
|
||||||
padding: 16px;
|
padding: 32px;
|
||||||
|
background: #fff;
|
||||||
|
color: #000;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-content {
|
.node-content {
|
||||||
padding: 32px;
|
justify-self: center;
|
||||||
|
padding: 0px 32px 32px 32px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: #333;
|
||||||
|
width: 100ex;
|
||||||
|
|
||||||
|
&[contenteditable] {
|
||||||
|
outline: 0px solid transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 100ex) {
|
||||||
|
.node-content {
|
||||||
|
width: initial;
|
||||||
|
justify-self: start;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
@fontsize: 14pt;
|
@fontsize: 14pt;
|
||||||
@background: #494949;
|
@background: #fff;
|
||||||
@accent_1: #ecbf00;
|
@accent_1: #ecbf00;
|
||||||
@accent_2: #abc837;
|
@accent_2: #abc837;
|
||||||
|
@accent_3: #c84a37;
|
||||||
|
Loading…
Reference in New Issue
Block a user