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 { constructor() {//{{{ super() this.nodeUI = createRef() this.reloadTree = signal(0) this.state = { startNode: null, } window._sync = new Sync() window._sync.run() this.getStartNode() }//}}} render(_props, { startNode }) {//{{{ console.log('notes2 render') const treeKey = `tree-${this.reloadTree}` console.log('treeKey', treeKey) if (startNode === null) return return html` <${Tree} app=${this} key=${treeKey} 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 }) }) }//}}} async goToNode(nodeUUID, dontPush, dontExpand) {//{{{ if (nodeUUID === null || nodeUUID === undefined) return // 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. const node = nodeStore.node(nodeUUID) const ancestors = await nodeStore.getNodeAncestry(node) this.nodeUI.current.setNode(node) this.nodeUI.current.setCrumbs(ancestors) this.tree.setSelected(node, dontExpand) }//}}} logout() {//{{{ localStorage.removeItem('session.UUID') location.href = '/' }//}}} } class Tree extends Component { constructor(props) {//{{{ super(props) console.log('new tree') this.treeNodeComponents = {} this.treeTrunk = [] this.selectedNode = null this.expandedNodes = {} // keyed on UUID this.treeDiv = createRef() 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.state.startNode?.UUID} />` }) return html`
${renderedTreeTrunk}
` }//}}} componentDidMount() {//{{{ this.treeDiv.current.addEventListener('keydown', event => this.keyHandler(event)) // This will show and select the treenode that is selected in the node UI. const node = _notes2.current?.nodeUI.current?.node.value if (node === null) return _notes2.current.tree.expandToTrunk(node) this.setSelected(node) }//}}} populateFirstLevel(callback = null) {//{{{ nodeStore.get(ROOT_NODE) .then(node => node.fetchChildren()) .then(children => { this.treeNodeComponents = {} this.treeTrunk = [] for (const node of children) { // The root node isn't supposed to be shown in the tree. if (node.UUID === ROOT_NODE) continue if (node.ParentUUID === ROOT_NODE) this.treeTrunk.push(node) } this.forceUpdate() if (callback) callback() }) .catch(e => { console.log(e); console.log(e.type, e.error); alert(e.error) }) }//}}} setSelected(node, dontExpand) {//{{{ // The previously selected node, if any, needs to be rerendered // to not retain its 'selected' class. const prevUUID = this.selectedNode?.UUID this.selectedNode = node if (prevUUID) this.treeNodeComponents[prevUUID]?.current.forceUpdate() // And now the newly selected node is rerendered. this.treeNodeComponents[node.UUID]?.current.forceUpdate() if (!dontExpand) this.setNodeExpanded(node.UUID, true) }//}}} isSelected(node) {//{{{ return this.selectedNode?.UUID === node.UUID }//}}} async expandToTrunk(node) {//{{{ // Get all ancestors from a certain node up to the highest grandparent. const ancestry = await nodeStore.getNodeAncestry(node, []) for (const i in ancestry) { await nodeStore.node(ancestry[i].UUID).fetchChildren() this.setNodeExpanded(ancestry[i].UUID, true) } // Already a top node, no need to expand anything. if (ancestry.length === 0) return // Start the chain of by expanding the top node. this.setNodeExpanded(ancestry[ancestry.length - 1].UUID, true) }//}}} getNodeExpanded(UUID) {//{{{ if (this.expandedNodes[UUID] === undefined) this.expandedNodes[UUID] = signal(false) return this.expandedNodes[UUID].value }//}}} setNodeExpanded(UUID, value) {//{{{ // Creating a default value if it doesn't exist already. this.getNodeExpanded(UUID) this.expandedNodes[UUID].value = value }//}}} getParentWithNextSibling(node) {//{{{ let currNode = node while (currNode !== null && currNode.UUID !== ROOT_NODE && currNode.getSiblingAfter() === null) { currNode = currNode.getParent() } return currNode?.getSiblingAfter() }//}}} getLastExpandedNode(node) {//{{{ let currNode = node while (this.getNodeExpanded(currNode.UUID) && currNode.hasChildren()) { currNode = currNode.Children[currNode.Children.length - 1] } return currNode }//}}} async keyHandler(event) {//{{{ let handled = true const n = this.selectedNode const Space = ' ' switch (event.key) { // Space is toggling expansion. case Space: case 'Enter': const expanded = this.getNodeExpanded(n.UUID) this.setNodeExpanded(n.UUID, !expanded) break case 'g': case 'Home': this.navigateTop() break case 'G': case 'End': this.navigateBottom() break case 'j': case 'ArrowDown': await this.navigateDown(this.selectedNode) break case 'k': case 'ArrowUp': await this.navigateUp(this.selectedNode) break case 'h': case 'ArrowLeft': await this.navigateLeft(this.selectedNode) break case 'l': case 'ArrowRight': await this.navigateRight(this.selectedNode) break default: // nonsole.log(event.key) handled = false } if (handled) { event.preventDefault() event.stopPropagation() } }//}}} async navigateLeft(n) {//{{{ if (n === null) return const expanded = this.getNodeExpanded(n.UUID) if (expanded && n.hasChildren()) { this.setNodeExpanded(n.UUID, false) return } if (n.isFirstSibling() && n.getParent().UUID !== ROOT_NODE) { await _notes2.current.goToNode(n.getParent()?.UUID, true, true) return } const siblingBefore = n.getSiblingBefore() const siblingExpanded = this.getNodeExpanded(siblingBefore?.UUID) if (siblingBefore !== null && siblingExpanded && siblingBefore.hasChildren()) { const siblingAbove = this.getLastExpandedNode(siblingBefore) await _notes2.current.goToNode(siblingAbove?.UUID, true, true) return } await _notes2.current.goToNode(n.getSiblingBefore()?.UUID, true, true) }//}}} async navigateRight(n) {//{{{ if (n === null) return const siblingAfter = n.getSiblingAfter() const expanded = this.getNodeExpanded(n.UUID) if (!expanded && n.hasChildren()) { this.setNodeExpanded(n.UUID, true) return } if (expanded && n.hasChildren()) { await _notes2.current.goToNode(n.Children[0]?.UUID, true, true) return } if (n.isLastSibling()) { const nextNode = this.getParentWithNextSibling(n) await _notes2.current.goToNode(nextNode?.UUID, true, true) return } await _notes2.current.goToNode(n.getSiblingAfter()?.UUID, true, true) }//}}} async navigateUp(n) {//{{{ if (n === null) return let parent = null const siblingBefore = n.getSiblingBefore() let siblingExpanded = false if (siblingBefore !== null) siblingExpanded = this.getNodeExpanded(siblingBefore.UUID) if (n.isFirstSibling()) { parent = n.getParent() if (parent?.UUID === ROOT_NODE) return await _notes2.current.goToNode(parent?.UUID, true, true) return } if (siblingBefore !== null && siblingExpanded && siblingBefore.hasChildren()) { await _notes2.current.goToNode(siblingBefore.Children[siblingBefore.Children.length - 1]?.UUID, true, true) return } if (siblingBefore) { await _notes2.current.goToNode(siblingBefore.UUID, true, true) return } }//}}} async navigateDown(n) {//{{{ if (n === null) return const nodeExpanded = this.getNodeExpanded(n.UUID) // Last node, not expanded, so it matters not whether it has children or not. // Traverse upward to nearest parent with next sibling. if (!nodeExpanded && n.isLastSibling()) { const wantedNode = this.getParentWithNextSibling(n) await _notes2.current.goToNode(wantedNode?.UUID, true, true) return } if (nodeExpanded && n.isLastSibling() && !n.hasChildren()) { const wantedNode = this.getParentWithNextSibling(n) await _notes2.current.goToNode(wantedNode?.UUID, true, true) return } // Node not expanded. Go to this node's next sibling. // GoToNode will abort if given null. if (!nodeExpanded || !n.hasChildren()) { await _notes2.current.goToNode(n.getSiblingAfter()?.UUID, true, true) return } // Node is expanded. // Children will be visually beneath this node, if any. if (nodeExpanded && n.hasChildren()) { await _notes2.current.goToNode(n.Children[0].UUID, true, true) return } }//}}} async navigateTop() {//{{{ const root = await nodeStore.get(ROOT_NODE) if (root.Children.length === 0) return await _notes2.current.goToNode(root.Children[0]?.UUID, true, true) }//}}} async navigateBottom() {//{{{ const root = await nodeStore.get(ROOT_NODE) if (root.Children.length === 0) return const toplevel = root.Children[root.Children.length - 1] const toplevelExpanded = this.getNodeExpanded(toplevel?.UUID) if (toplevelExpanded) { const lastnode = this.getLastExpandedNode(toplevel) await _notes2.current.goToNode(lastnode?.UUID, true, true) } else await _notes2.current.goToNode(root.Children[root.Children.length - 1]?.UUID, true, true) }//}}} } class TreeNode extends Component { constructor(props) {//{{{ super(props) this.children_populated = signal(false) if (this.props.node.Level === 0 || this.props.tree.getNodeExpanded(this.props.node.UUID)) 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. const selected = tree.isSelected(node) ? 'selected' : '' if (!this.children_populated.value && tree.getNodeExpanded(parent?.props.node.UUID)) 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 (tree.getNodeExpanded(node.UUID)) expandImg = html`` else expandImg = html`` } return html`
{ tree.setNodeExpanded(node.UUID, !tree.getNodeExpanded(node.UUID)) }}>${expandImg}
window._notes2.current.goToNode(node.UUID)}>${node.get('Name')}
${children}
` }//}}} async fetchChildren() {//{{{ await this.props.node.fetchChildren() this.children_populated.value = true }//}}} } // vim: foldmethod=marker