419 lines
12 KiB
JavaScript
419 lines
12 KiB
JavaScript
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`
|
|
<div id="tree" ref=${this.treeDiv} tabindex="0">
|
|
<div id="logo" onclick=${() => _notes2.current.goToNode(ROOT_NODE)}><img src="/images/${_VERSION}/logo.svg" /></div>
|
|
<div class="icons">
|
|
<img src="/images/${_VERSION}/icon_refresh.svg" />
|
|
</div>
|
|
${renderedTreeTrunk}
|
|
</div>`
|
|
}//}}}
|
|
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`<img src="/images/${window._VERSION}/leaf.svg" />`
|
|
else {
|
|
if (tree.getNodeExpanded(node.UUID))
|
|
expandImg = html`<img src="/images/${window._VERSION}/expanded.svg" />`
|
|
else
|
|
expandImg = html`<img src="/images/${window._VERSION}/collapsed.svg" />`
|
|
}
|
|
|
|
return html`
|
|
<div class="node">
|
|
<div class="expand-toggle" onclick=${() => { tree.setNodeExpanded(node.UUID, !tree.getNodeExpanded(node.UUID)) }}>${expandImg}</div>
|
|
<div class="name ${selected}" onclick=${() => window._notes2.current.goToNode(node.UUID)}>${node.get('Name')}</div>
|
|
<div class="children ${node.Children.length > 0 && tree.getNodeExpanded(node.UUID) ? 'expanded' : 'collapsed'}">${children}</div>
|
|
</div>`
|
|
}//}}}
|
|
async fetchChildren() {//{{{
|
|
await this.props.node.fetchChildren()
|
|
this.children_populated.value = true
|
|
}//}}}
|
|
}
|
|
|
|
// vim: foldmethod=marker
|