diff --git a/static/favicon.ico b/static/favicon.ico deleted file mode 100644 index 299310f..0000000 Binary files a/static/favicon.ico and /dev/null differ diff --git a/static/js/notes2.mjs b/static/js/notes2.mjs index ddaf891..50286af 100644 --- a/static/js/notes2.mjs +++ b/static/js/notes2.mjs @@ -86,6 +86,404 @@ export class Notes2 extends Component { }//}}} } +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() + + // childrenFetchedCallbacks is keyed on a UUID and each + // item is an array with callbacks called when a UUID has + // had all children fetched. + this.childrenFetchedCallbacks = {} + + 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` +
+ +
+ _mbus.dispatch('op-search')} /> + _sync.run()} /> +
+ ${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) + }//}}} + + fetchChildrenNotify(uuid, fn) {//{{{ + if (this.childrenFetchedCallbacks[uuid] === undefined) + this.childrenFetchedCallbacks[uuid] = [fn] + else + this.childrenFetchedCallbacks[uuid].push(fn) + }//}}} + fetchChildrenOn(uuid) {//{{{ + if (this.childrenFetchedCallbacks[uuid] === undefined) + return + for (const fn of this.childrenFetchedCallbacks[uuid]) + fn(uuid) + delete this.childrenFetchedCallbacks[uuid] + }//}}} + + 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, 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], 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], true) + }//}}} + getNodeExpanded(UUID) {//{{{ + if (this.expandedNodes[UUID] === undefined) + this.expandedNodes[UUID] = signal(false) + return this.expandedNodes[UUID].value + }//}}} + async setNodeExpanded(node, value) {//{{{ + return new Promise((resolve, reject) => { + const work = uuid => { + // Creating a default value if it doesn't exist already. + this.getNodeExpanded(uuid) + this.expandedNodes[uuid].value = value + resolve() + } + + if (node.hasFetchedChildren()) { + work(node.UUID) + return + } else { + this.fetchChildrenNotify(node.UUID, uuid => work(uuid)) + } + }) + }//}}} + 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 recursiveExpand(node, state) {//{{{ + if (state) + await this.setNodeExpanded(node, true) + + for (const child of node.Children) + await this.recursiveExpand(child, state) + + if (!state) + await this.setNodeExpanded(node, false) + }//}}} + + async keyHandler(event) {//{{{ + let handled = true + const n = this.selectedNode + const Space = ' ' + + // This handler would otherwise react to stuff like Ctrl+L. + if (event.ctrlKey || event.altKey) + return + + switch (event.key) { + // Space and enter is toggling expansion. + // Holding shift down does it recursively. + case Space: + case 'Enter': + const expanded = this.getNodeExpanded(n.UUID) + if (event.shiftKey) { + this.recursiveExpand(n, !expanded) + } else { + this.setNodeExpanded(n, !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, 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, 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, !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 + }//}}} +} + class Op { constructor(id) { this.id = id