Notes/static/js/node.mjs

365 lines
9.2 KiB
JavaScript
Raw Normal View History

2023-06-18 20:13:35 +02:00
import { h, Component, createRef } from 'preact'
2023-06-17 09:11:14 +02:00
import htm from 'htm'
import { signal } from 'preact/signals'
const html = htm.bind(h)
export class NodeUI extends Component {
constructor() {//{{{
super()
2023-06-18 22:05:10 +02:00
this.menu = signal(false)
this.tree = signal(null)
2023-06-17 09:11:14 +02:00
this.node = signal(null)
2023-06-18 20:13:35 +02:00
this.nodeContent = createRef()
2023-06-21 23:52:21 +02:00
this.upload = signal(false)
2023-06-17 09:11:14 +02:00
window.addEventListener('popstate', evt=>{
if(evt.state && evt.state.hasOwnProperty('nodeID'))
this.goToNode(evt.state.nodeID, true)
else
this.goToNode(0, true)
})
2023-06-20 07:59:54 +02:00
window.addEventListener('keydown', evt=>this.keyHandler(evt))
2023-06-17 09:11:14 +02:00
}//}}}
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)
2023-06-17 09:11:14 +02:00
let crumbs = [
html`<div class="crumb" onclick=${()=>this.goToNode(0)}>Start</div>`
]
2023-06-18 20:13:35 +02:00
crumbs = crumbs.concat(node.Crumbs.slice(0).map(node=>
2023-06-17 09:11:14 +02:00
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>
`)
2023-06-18 20:13:35 +02:00
let modified = ''
if(this.props.app.nodeModified.value)
modified = 'modified';
2023-06-21 23:52:21 +02:00
let upload = '';
if(this.upload.value)
upload = html`<${UploadUI} app=${this} />`
let menu = '';
if(this.menu.value)
upload = html`<${Menu} app=${this} />`
2023-06-18 22:05:10 +02:00
2023-06-21 23:52:21 +02:00
return html`
${menu}
${upload}
2023-06-18 20:13:35 +02:00
<header class="${modified}" onclick=${()=>this.saveNode()}>
<div class="name">Notes</div>
2023-06-20 08:13:32 +02:00
<div class="add" onclick=${evt=>this.createNode(evt)}>+</div>
<div class="menu" onclick=${evt=>this.showMenu(evt)}></div>
2023-06-18 20:13:35 +02:00
</header>
2023-06-18 22:05:10 +02:00
2023-06-18 20:13:35 +02:00
<div class="crumbs">${crumbs}</crumbs>
2023-06-18 22:05:10 +02:00
2023-06-17 09:11:14 +02:00
${children.length > 0 ? html`<div class="child-nodes">${children}</div>` : html``}
2023-06-18 22:05:10 +02:00
${node.ID > 0 ? html`
<div class="node-name">${node.Name}</div>
<${NodeContent} key=${node.ID} content=${node.Content} ref=${this.nodeContent} />
` : html``}
2023-06-17 09:11:14 +02:00
`
}//}}}
componentDidMount() {//{{{
2023-06-18 20:13:35 +02:00
let urlParams = new URLSearchParams(window.location.search)
let nodeID = urlParams.get('node')
let root = new Node(this.props.app, nodeID ? parseInt(nodeID) : 0)
2023-06-17 09:11:14 +02:00
root.retrieve(node=>{
this.node.value = node
})
}//}}}
2023-06-18 22:05:10 +02:00
2023-06-20 07:59:54 +02:00
keyHandler(evt) {//{{{
let handled = true
switch(evt.key.toUpperCase()) {
case 'S':
2023-06-21 23:52:21 +02:00
if(evt.ctrlKey || (evt.shiftKey && evt.altKey))
this.saveNode()
2023-06-20 07:59:54 +02:00
break
case 'N':
2023-06-21 23:52:21 +02:00
if((evt.ctrlKey && evt.AltKey) || (evt.shiftKey && evt.altKey))
this.createNode()
2023-06-20 07:59:54 +02:00
break
2023-06-21 23:52:21 +02:00
case 'U':
if((evt.ctrlKey && evt.altKey) || (evt.shiftKey && evt.altKey))
this.upload.value = true
2023-06-20 07:59:54 +02:00
default:
handled = false
}
if(handled) {
evt.preventDefault()
evt.stopPropagation()
}
}//}}}
2023-06-20 08:13:32 +02:00
showMenu(evt) {//{{{
evt.stopPropagation()
2023-06-18 22:05:10 +02:00
this.menu.value = true
}//}}}
2023-06-20 08:13:32 +02:00
logout() {//{{{
window.localStorage.removeItem('session.UUID')
location.href = '/'
}//}}}
2023-06-18 22:05:10 +02:00
2023-06-17 09:11:14 +02:00
goToNode(nodeID, dontPush) {//{{{
2023-06-18 20:13:35 +02:00
if(this.props.app.nodeModified.value) {
if(!confirm("Changes not saved. Do you want to discard changes?"))
return
}
2023-06-17 09:11:14 +02:00
if(!dontPush)
2023-06-18 20:13:35 +02:00
history.pushState({ nodeID }, '', `/?node=${nodeID}`)
2023-06-17 09:11:14 +02:00
let node = new Node(this.props.app, nodeID)
node.retrieve(node=>{
2023-06-18 20:13:35 +02:00
this.props.app.nodeModified.value = false
2023-06-17 09:11:14 +02:00
this.node.value = node
})
}//}}}
2023-06-20 08:13:32 +02:00
createNode(evt) {//{{{
2023-06-21 23:52:21 +02:00
if(evt)
evt.stopPropagation()
2023-06-18 20:13:35 +02:00
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)
}//}}}
2023-06-18 22:05:10 +02:00
saveNode() {//{{{
let content = this.nodeContent.current.contentDiv.current.value
2023-06-18 20:13:35 +02:00
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)
2023-06-18 22:05:10 +02:00
}//}}}
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>`)
}//}}}
2023-06-18 20:13:35 +02:00
}
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";
2023-06-18 20:13:35 +02:00
}//}}}
2023-06-17 09:11:14 +02:00
}
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)
}//}}}
}
2023-06-21 23:52:21 +02:00
class Menu extends Component {
render({ app }) {//{{{
return html`
<div id="blackout" onclick=${()=>app.menu.value = false}></div>
<div id="menu" class="${app.menu.value ? 'show' : ''}">
<div class="item" onclick=${()=>app.renameNode()}>Rename</div>
<div class="item separator" onclick=${()=>app.deleteNode()}>Delete</div>
<div class="item separator" onclick=${()=>{ app.upload.value = true; app.menu.value = false }}>Upload</div>
<div class="item" onclick=${()=>app.logout()}>Log out</div>
</div>
`
}//}}}
}
class UploadUI extends Component {
constructor() {//{{{
super()
this.file = createRef()
this.filelist = signal([])
this.fileRefs = []
this.progressRefs = []
}//}}}
render({ nodeui }) {//{{{
let filelist = this.filelist.value
let files = []
for(let i = 0; i < filelist.length; i++) {
files.push(html`<div key=file_${i} ref=${this.fileRefs[i]} class="file">${filelist.item(i).name}</div><div class="progress" ref=${this.progressRefs[i]}></div>`)
}
return html`
<div id="blackout" onclick=${()=>nodeui.upload.value = false}></div>
<div id="upload">
<input type="file" ref=${this.file} onchange=${()=>this.upload()} multiple />
<div class="files">
${files}
</div>
</div>
`
}//}}}
componentDidMount() {//{{{
this.file.current.focus()
}//}}}
upload() {//{{{
this.fileRefs = []
this.progressRefs = []
let input = this.file.current
this.filelist.value = input.files
for(let i = 0; i < input.files.length; i++) {
this.fileRefs.push(createRef())
this.progressRefs.push(createRef())
this.postFile(input.files[i], progress=>{
this.progressRefs[i].current.innerHTML = `${progress}%`
}, ()=>{
this.fileRefs[i].current.classList.add("done")
this.progressRefs[i].current.classList.add("done")
})
}
}//}}}
postFile(file, progressCallback, doneCallback) {//{{{
var formdata = new FormData()
formdata.append('file', file)
var request = new XMLHttpRequest()
request.addEventListener("error", ()=>{
window._app.current.responseError({ upload: "An unknown error occured" })
})
request.addEventListener("loadend", ()=>{
if(request.status != 200) {
window._app.current.responseError({ upload: request.statusText })
return
}
let response = JSON.parse(request.response)
if(!response.OK) {
window._app.current.responseError({ upload: response.Error })
return
}
doneCallback()
})
request.upload.addEventListener('progress', evt=>{
var fileSize = file.size
if(evt.loaded <= fileSize)
progressCallback(Math.round(evt.loaded / fileSize * 100))
if(evt.loaded == evt.total)
progressCallback(100)
})
request.open('post', '/node/upload')
request.setRequestHeader("X-Session-Id", window._app.current.session.UUID)
//request.timeout = 45000
request.send(formdata)
}//}}}
}
2023-06-17 09:11:14 +02:00
// vim: foldmethod=marker