254 lines
6.2 KiB
JavaScript
254 lines
6.2 KiB
JavaScript
import { h, Component, createRef } from 'preact'
|
|
import htm from 'htm'
|
|
import { signal } from 'preact/signals'
|
|
const html = htm.bind(h)
|
|
|
|
export class NodeUI extends Component {
|
|
constructor() {//{{{
|
|
super()
|
|
this.menu = signal(false)
|
|
this.tree = signal(null)
|
|
this.node = signal(null)
|
|
this.nodeContent = createRef()
|
|
window.addEventListener('popstate', evt=>{
|
|
if(evt.state && evt.state.hasOwnProperty('nodeID'))
|
|
this.goToNode(evt.state.nodeID, true)
|
|
else
|
|
this.goToNode(0, true)
|
|
})
|
|
window.addEventListener('keydown', evt=>this.keyHandler(evt))
|
|
}//}}}
|
|
render() {//{{{
|
|
if(this.node.value === null)
|
|
return
|
|
|
|
let node = this.node.value
|
|
let tree = this.tree.value
|
|
|
|
let treeHTML = html`Tree`
|
|
if(tree !== null)
|
|
treeHTML = this.renderTree(tree)
|
|
|
|
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())
|
|
|
|
let children = node.Children.sort((a,b)=>{
|
|
if(a.Name.toLowerCase() > b.Name.toLowerCase()) return 1;
|
|
if(a.Name.toLowerCase() < b.Name.toLowerCase()) return -1;
|
|
return 0
|
|
}).map(child=>html`
|
|
<div class="child-node" onclick=${()=>this.goToNode(child.ID)}>${child.Name}</div>
|
|
`)
|
|
|
|
let modified = ''
|
|
if(this.props.app.nodeModified.value)
|
|
modified = 'modified';
|
|
|
|
return html`
|
|
<div id="menu-blackout" class="${this.menu.value ? 'show' : ''}" onclick=${()=>this.menu.value = false}></div>
|
|
<div id="menu" class="${this.menu.value ? 'show' : ''}">
|
|
<div class="item" onclick=${()=>this.renameNode()}>Rename</div>
|
|
<div class="item" onclick=${()=>this.deleteNode()}>Delete</div>
|
|
</div>
|
|
|
|
<header class="${modified}" onclick=${()=>this.saveNode()}>
|
|
<div class="name">Notes</div>
|
|
<div class="add" onclick=${()=>this.createNode()}>+</div>
|
|
<div class="menu" onclick=${()=>this.showMenu()}>☰</div>
|
|
</header>
|
|
|
|
<div class="crumbs">${crumbs}</crumbs>
|
|
|
|
${children.length > 0 ? html`<div class="child-nodes">${children}</div>` : html``}
|
|
|
|
${node.ID > 0 ? html`
|
|
<div class="node-name">${node.Name}</div>
|
|
<${NodeContent} key=${node.ID} content=${node.Content} ref=${this.nodeContent} />
|
|
` : html``}
|
|
`
|
|
}//}}}
|
|
componentDidMount() {//{{{
|
|
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=>{
|
|
this.node.value = node
|
|
})
|
|
}//}}}
|
|
|
|
keyHandler(evt) {//{{{
|
|
let handled = true
|
|
switch(evt.key.toUpperCase()) {
|
|
case 'S':
|
|
if(!evt.ctrlKey) {
|
|
handled = false
|
|
break
|
|
}
|
|
this.saveNode()
|
|
break
|
|
|
|
case 'N':
|
|
if(!evt.ctrlKey && !evt.AltKey) {
|
|
handled = false
|
|
break
|
|
}
|
|
this.createNode()
|
|
break
|
|
|
|
default:
|
|
handled = false
|
|
}
|
|
|
|
if(handled) {
|
|
evt.preventDefault()
|
|
evt.stopPropagation()
|
|
}
|
|
}//}}}
|
|
showMenu() {//{{{
|
|
this.menu.value = true
|
|
}//}}}
|
|
|
|
goToNode(nodeID, dontPush) {//{{{
|
|
if(this.props.app.nodeModified.value) {
|
|
if(!confirm("Changes not saved. Do you want to discard changes?"))
|
|
return
|
|
}
|
|
|
|
if(!dontPush)
|
|
history.pushState({ nodeID }, '', `/?node=${nodeID}`)
|
|
let node = new Node(this.props.app, nodeID)
|
|
node.retrieve(node=>{
|
|
this.props.app.nodeModified.value = false
|
|
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.value
|
|
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)
|
|
}//}}}
|
|
renameNode() {//{{{
|
|
let name = prompt("New name")
|
|
if(!name)
|
|
return
|
|
|
|
this.props.app.request('/node/rename', {
|
|
Name: name.trim(),
|
|
NodeID: this.node.value.ID,
|
|
})
|
|
.then(_=>{
|
|
this.goToNode(this.node.value.ID)
|
|
this.menu.value = false
|
|
})
|
|
.catch(this.props.app.responseError)
|
|
}//}}}
|
|
deleteNode() {//{{{
|
|
if(!confirm("Do you want to delete this note and all sub-notes?"))
|
|
return
|
|
|
|
this.props.app.request('/node/delete', {
|
|
NodeID: this.node.value.ID,
|
|
})
|
|
.then(_=>{
|
|
this.goToNode(this.node.value.ParentID)
|
|
this.menu.value = false
|
|
})
|
|
.catch(this.props.app.responseError)
|
|
}//}}}
|
|
retrieveTree() {//{{{
|
|
this.props.app.request('/node/tree', { StartNodeID: this.node.value.ID })
|
|
.then(res=>{
|
|
this.tree.value = res.Nodes
|
|
})
|
|
.catch(this.props.app.responseError)
|
|
}//}}}
|
|
renderTree(tree) {//{{{
|
|
return tree.map(node=>html`<div class="node" style="margin-left: ${(node.Level+1) * 32}px">${node.Name}</div>`)
|
|
}//}}}
|
|
}
|
|
|
|
class NodeContent extends Component {
|
|
constructor(props) {//{{{
|
|
super(props)
|
|
this.contentDiv = createRef()
|
|
this.state = {
|
|
modified: false,
|
|
//content: props.content,
|
|
}
|
|
}//}}}
|
|
render({ content }) {//{{{
|
|
return html`
|
|
<textarea class="node-content" ref=${this.contentDiv} oninput=${()=>this.contentChanged()} required rows=1>${content}</textarea>
|
|
`
|
|
}//}}}
|
|
componentDidMount() {//{{{
|
|
this.resize()
|
|
}//}}}
|
|
componentDidUpdate() {//{{{
|
|
this.resize()
|
|
}//}}}
|
|
contentChanged() {//{{{
|
|
window._app.current.nodeModified.value = true
|
|
this.resize()
|
|
}//}}}
|
|
resize() {//{{{
|
|
let textarea = this.contentDiv.current;
|
|
textarea.style.height = "auto";
|
|
textarea.style.height = textarea.scrollHeight + 16 + "px";
|
|
}//}}}
|
|
}
|
|
|
|
class Node {
|
|
constructor(app, nodeID) {//{{{
|
|
this.app = app
|
|
this.ID = nodeID
|
|
this.ParentID = 0
|
|
this.UserID = 0
|
|
this.Name = ''
|
|
this.Content = ''
|
|
this.Children = []
|
|
this.Crumbs = []
|
|
}//}}}
|
|
retrieve(callback) {//{{{
|
|
this.app.request('/node/retrieve', { ID: this.ID })
|
|
.then(res=>{
|
|
this.ParentID = res.Node.ParentID
|
|
this.UserID = res.Node.UserID
|
|
this.Name = res.Node.Name
|
|
this.Content = res.Node.Content
|
|
this.Children = res.Node.Children
|
|
this.Crumbs = res.Node.Crumbs
|
|
callback(this)
|
|
})
|
|
.catch(this.app.responseError)
|
|
}//}}}
|
|
}
|
|
|
|
// vim: foldmethod=marker
|