import { h, Component, createRef } from 'preact' import { signal } from 'preact/signals' import htm from 'htm' import { Node, NodeUI } from 'node' import { ROOT_NODE } from 'node_store' const html = htm.bind(h) export class Notes2 extends Component { state = { startNode: null, } constructor() {//{{{ super() this.tree = createRef() this.nodeUI = createRef() this.getStartNode() }//}}} render({}, { startNode }) {//{{{ if (startNode === null) return return html` <${Tree} ref=${this.tree} app=${this} startNode=${startNode} />
<${NodeUI} app=${this} ref=${this.nodeUI} startNode=${startNode} />
` }//}}} 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] nodeStore.get(nodeUUID).then(node => { this.setState({ startNode: node }) }) }//}}} goToNode(nodeUUID, dontPush) {//{{{ // Don't switch notes until saved. if (this.nodeUI.current.nodeModified.value) { if (!confirm("Changes not saved. Do you want to discard changes?")) return } if (!dontPush) history.pushState({ nodeUUID }, '', `/notes2#${nodeUUID}`) // New node is fetched in order to retrieve content and files. // Such data is unnecessary to transfer for tree/navigational purposes. nodeStore.get(nodeUUID).then(node => { this.nodeUI.current.setNode(node) //this.showPage('node') }) }//}}} logout() {//{{{ localStorage.removeItem('session.UUID') location.href = '/' }//}}} } class Tree extends Component { constructor(props) {//{{{ super(props) this.treeNodes = {} this.treeNodeComponents = {} this.treeTrunk = [] this.selectedTreeNode = null this.props.app.tree = this this.populateFirstLevel() }//}}} render({ app }) {//{{{ const renderedTreeTrunk = this.treeTrunk.map(node => { 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.startNode?.UUID} />` }) return html`
${renderedTreeTrunk}
` }//}}} populateFirstLevel(callback = null) {//{{{ nodeStore.getTreeNodes('', 0) .then(async res => { res.sort(Node.sort) this.treeNodes = {} this.treeNodeComponents = {} this.treeTrunk = [] this.selectedTreeNode = null // 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. for (const node of res) { this.treeNodes[node.UUID] = node if (node.ParentUUID === '') this.treeTrunk.push(node) else if (this.treeNodes[node.ParentUUD] !== undefined) this.treeNodes[node.ParentUUID].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. // XXX this.crumbsUpdateNodes() this.forceUpdate() if (callback) callback() }) .catch(e => { console.log(e); console.log(e.type, e.error); alert(e.error) }) }//}}} setSelected(node) {//{{{ return // TODO 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) {//{{{ console.log('crumbs', this.props.app.startNode.Crumbs) for (const crumb in this.props.app.startNode.Crumbs) { // Start node is loaded before the tree. const node = this.treeNodes[crumb.ID] if (node) node._expanded = true // Tree is done before the start node. const 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(nodeUUID) {//{{{ let node = this.treeNodes[nodeUUID] if (node === undefined) return node = this.treeNodes[node.ParentUUID] while (node !== undefined) { this.treeNodeComponents[node.UUID].current.expanded.value = true node = this.treeNodes[node.ParentUUID] } }//}}} } class TreeNode extends Component { constructor(props) {//{{{ super(props) this.selected = signal(props.selected) this.expanded = signal(this.props.node._expanded) this.children_populated = signal(false) if (this.props.node.Level === 0) this.fetchChildren() }//}}} render({ tree, node, parent }) {//{{{ // Fetch the next level of children if the parent tree node is expanded and our children thus will be visible. if (!this.children_populated.value && parent?.expanded.value) this.fetchChildren() const children = node.Children.map(node => { tree.treeNodeComponents[node.UUID] = createRef() return html`<${TreeNode} key=${`treenode_${node.UUID}`} tree=${tree} node=${node} parent=${this} ref=${tree.treeNodeComponents[node.UUID]} selected=${node.UUID === tree.props.app.startNode?.UUID} />` }) let expandImg = '' if (node.Children.length === 0) expandImg = html`` else { if (this.expanded.value) expandImg = html`` else expandImg = html`` } const selected = (this.selected.value ? 'selected' : '') return html`
{ this.expanded.value ^= true }}>${expandImg}
window._notes2.current.goToNode(node.UUID)}>${node.Name}
${children}
` }//}}} fetchChildren() {//{{{ this.props.node.fetchChildren().then(() => { this.children_populated.value = true }) }//}}} } // vim: foldmethod=marker