import 'preact/devtools' import { signal } from 'preact/signals' import { h, Component, render, createRef } from 'preact' import htm from 'htm' import { Session } from 'session' import { Node, NodeUI } from 'node' const html = htm.bind(h) class App extends Component { constructor() {//{{{ super() this.websocket_uri = `ws://localhost:1371/ws` this.websocket = null this.websocket_int_ping = null this.websocket_int_reconnect = null this.wsConnect() this.wsLoop() this.session = new Session(this) this.session.initialize() this.login = createRef() this.tree = null this.nodeUI = createRef() this.nodeModified = signal(false) this.startNode = null this.setStartNode() }//}}} render() {//{{{ let app_el = document.getElementById('app') if(!this.session.initialized) { return html`
Validating session
` } if(!this.session.authenticated()) { app_el.classList.remove('node') app_el.classList.add('login') return html`<${Login} ref=${this.login} />` } app_el.classList.remove('login') app_el.classList.add('node') return html` <${Tree} app=${this} ref=${this.tree} /> <${NodeUI} app=${this} ref=${this.nodeUI} /> ` }//}}} wsLoop() {//{{{ setInterval(()=>{ if(this.websocket === null) { console.log("wsLoop connect") this.wsConnect() } }, 1000) }//}}} wsConnect() {//{{{ this.websocket = new WebSocket(this.websocket_uri) this.websocket.onopen = evt=>this.wsOpen(evt) this.websocket.onmessage = evt=>this.wsMessage(evt) this.websocket.onerror = evt=>this.wsError(evt) this.websocket.onclose = evt=>this.wsClose(evt) }//}}} wsOpen() {//{{{ this.setState({ wsConnected: true }) // A ping interval to implement a rudimentary keep-alive. }//}}} wsClose() {//{{{ console.log("Lost connection to Notes server") this.websocket = null }//}}} wsError(evt) {//{{{ console.log("websocket error", evt) this.websocket = null; }//}}} wsMessage(evt) {//{{{ let msg = JSON.parse(evt.data) // Broadcast message if(msg.ID == '') { this.broadcastHandler(msg) } else { this.msgHandler(msg) } }//}}} responseError({comm, app, upload}) {//{{{ if(comm !== undefined) { comm.text().then(body=>alert(body)) return } if(app !== undefined && app.hasOwnProperty('Error')) { alert(app.Error) return } if(app !== undefined) { alert(JSON.stringify(app)) } if(upload !== undefined) { alert(upload) return } }//}}} async request(url, params) {//{{{ return new Promise((resolve, reject)=>{ let headers = { 'Content-Type': 'application/json', } if(this.session.UUID !== '') headers['X-Session-Id'] = this.session.UUID fetch(url, { method: 'POST', headers, body: JSON.stringify(params), }) .then(response=>{ // A HTTP communication level error occured if(!response.ok || response.status != 200) return reject({comm: response}) return response.json() }) .then(json=>{ // An application level error occured if(!json.OK) { return reject({app: json}) } return resolve(json) }) .catch(err=>reject({comm: err})) }) }//}}} broadcastHandler(msg) {//{{{ switch(msg.Op) { case 'css_reload': refreshCSS() break; } }//}}} setStartNode() {//{{{ let urlParams = new URLSearchParams(window.location.search) let nodeID = urlParams.get('node') this.startNode = new Node(this, nodeID ? parseInt(nodeID) : 0) }//}}} } class Login extends Component { constructor() {//{{{ super() this.authentication_failed = signal(false) this.state = { username: '', password: '', } }//}}} render({}, { username, password }) {//{{{ let authentication_failed = html``; if(this.authentication_failed.value) authentication_failed = html`
Authentication failed
`; return html`

Notes

this.setState({ username: evt.target.value})} onkeydown=${evt=>{ if(evt.code == 'Enter') this.login() }} /> this.setState({ password: evt.target.value})} onkeydown=${evt=>{ if(evt.code == 'Enter') this.login() }} /> ${authentication_failed}
` }//}}} componentDidMount() {//{{{ document.getElementById('username').focus() }//}}} login() {//{{{ let username = document.getElementById('username').value let password = document.getElementById('password').value window._app.current.session.authenticate(username, password) }//}}} } class Tree extends Component { constructor(props) {//{{{ super(props) this.treeNodes = {} this.treeNodeComponents = {} this.treeTrunk = [] this.selectedTreeNode = null this.props.app.tree = this this.props.app.request('/node/tree', { StartNodeID: 0 }) .then(res=>{ // A tree of nodes is built. This requires the list of nodes // returned from the server to be sorted in such a way that // a parent node always appears before a child node. // The server uses a recursive SQL query delivering this. res.Nodes.forEach(nodeData=>{ let node = new Node( this, nodeData.ID, ) node.Children = [] node.Crumbs = [] node.Files = [] node.Level = nodeData.Level node.Name = nodeData.Name node.ParentID = nodeData.ParentID node.Updated = nodeData.Updated node.UserID = nodeData.UserID this.treeNodes[node.ID] = node if(node.ParentID == 0) this.treeTrunk.push(node) else if(this.treeNodes[node.ParentID] !== undefined) this.treeNodes[node.ParentID].Children.push(node) }) // When starting with an explicit node value, expanding all nodes // on its path gives the user a sense of location. Not necessarily working // as the start node isn't guaranteed to have returned data yet. this.crumbsUpdateNodes() this.forceUpdate() }) .catch(this.responseError) }//}}} render({ app }) {//{{{ let renderedTreeTrunk = this.treeTrunk.map(node=>{ this.treeNodeComponents[node.ID] = createRef() return html`<${TreeNode} key=${"treenode_"+node.ID} tree=${this} node=${node} ref=${this.treeNodeComponents[node.ID]} selected=${node.ID == app.startNode.ID} />` }) return html`
${renderedTreeTrunk}
` }//}}} setSelected(node) {//{{{ if(this.selectedTreeNode) this.selectedTreeNode.selected.value = false this.selectedTreeNode = this.treeNodeComponents[node.ID].current this.selectedTreeNode.selected.value = true this.selectedTreeNode.expanded.value = true this.expandToTrunk(node.ID) }//}}} crumbsUpdateNodes(node) {//{{{ this.props.app.startNode.Crumbs.forEach(crumb=>{ // Start node is loaded before the tree. let node = this.treeNodes[crumb.ID] if(node) node._expanded = true // Tree is done before the start node. let component = this.treeNodeComponents[crumb.ID] if(component && component.current) component.current.expanded.value = true }) // Will be undefined when called from tree initialization // (as tree nodes aren't rendered yet) if(node !== undefined) this.setSelected(node) }//}}} expandToTrunk(nodeID) {//{{{ let node = this.treeNodes[nodeID] if(node === undefined) return node = this.treeNodes[node.ParentID] while(node !== undefined) { this.treeNodeComponents[node.ID].current.expanded.value = true node = this.treeNodes[node.ParentID] } }//}}} } class TreeNode extends Component { constructor(props) {//{{{ super(props) this.selected = signal(props.selected) this.expanded = signal(this.props.node._expanded) }//}}} render({ tree, node }) {//{{{ let children = node.Children.map(node=>{ tree.treeNodeComponents[node.ID] = createRef() return html`<${TreeNode} key=${"treenode_"+node.ID} tree=${tree} node=${node} ref=${tree.treeNodeComponents[node.ID]} selected=${node.ID == tree.props.app.startNode.ID} />` }) let expandImg = '' if(node.Children.length == 0) expandImg = html`` else { if(this.expanded.value) expandImg = html`` else expandImg = html`` } let selected = (this.selected.value ? 'selected' : '') return html`
this.expanded.value ^= true}>${expandImg}
window._app.current.nodeUI.current.goToNode(node.ID)}>${node.Name}
${children}
` }//}}} } // Init{{{ window._app = createRef() window._resourceModels = [] render(html`<${App} ref=${window._app} />`, document.getElementById('app')) //}}} // vim: foldmethod=marker