Notes2/static/js/notes2.mjs

201 lines
6.3 KiB
JavaScript
Raw Normal View History

2024-11-28 18:11:14 +01:00
import { h, Component, createRef } from 'preact'
2024-11-29 09:15:42 +01:00
import { signal } from 'preact/signals'
2024-11-28 18:11:14 +01:00
import htm from 'htm'
2024-12-02 15:26:35 +01:00
import { Node, NodeUI } from 'node'
2024-12-03 22:08:45 +01:00
import { ROOT_NODE } from 'node_store'
2024-11-28 18:11:14 +01:00
const html = htm.bind(h)
2024-12-03 22:08:45 +01:00
export class Notes2 extends Component {
2024-11-28 18:11:14 +01:00
constructor() {//{{{
2024-12-03 22:08:45 +01:00
super()
2024-12-02 15:26:35 +01:00
this.nodeUI = createRef()
2024-12-18 19:12:10 +01:00
this.state = {
startNode: null,
}
Sync.nodes().then(durationNodes =>
console.log(`Total time: ${Math.round(100 * durationNodes) / 100}s`)
)
2024-12-03 22:08:45 +01:00
this.getStartNode()
2024-11-28 18:11:14 +01:00
}//}}}
2024-12-18 19:12:10 +01:00
render(_props, { startNode }) {//{{{
2024-12-03 22:08:45 +01:00
if (startNode === null)
return
2024-11-28 18:11:14 +01:00
return html`
<${Tree} app=${this} startNode=${startNode} />
2024-12-03 22:08:45 +01:00
<div id="nodeui">
<${NodeUI} app=${this} ref=${this.nodeUI} startNode=${startNode} />
2024-12-03 06:53:31 +01:00
</div>
2024-11-28 18:11:14 +01:00
`
}//}}}
2024-12-03 22:08:45 +01:00
getStartNode() {//{{{
let nodeUUID = ROOT_NODE
2024-12-03 06:53:31 +01:00
2024-12-03 22:08:45 +01:00
// Is a UUID provided on the URI as an anchor?
2024-12-03 06:53:31 +01:00
const parts = document.URL.split('#')
2024-12-03 22:08:45 +01:00
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]
2024-12-03 06:53:31 +01:00
2024-12-03 22:08:45 +01:00
nodeStore.get(nodeUUID).then(node => {
this.setState({ startNode: node })
})
2024-11-29 09:15:42 +01:00
}//}}}
2024-12-19 23:13:41 +01:00
async goToNode(nodeUUID, dontPush) {//{{{
2024-12-03 22:08:45 +01:00
// Don't switch notes until saved.
if (this.nodeUI.current.nodeModified.value) {
if (!confirm("Changes not saved. Do you want to discard changes?"))
return
}
2024-11-28 18:11:14 +01:00
2024-12-03 22:08:45 +01:00
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)
2024-12-19 23:13:41 +01:00
const ancestors = await nodeStore.getNodeAncestry(node)
this.nodeUI.current.setNode(node)
2024-12-19 23:13:41 +01:00
this.nodeUI.current.setCrumbs(ancestors)
this.tree.setSelected(node)
2024-12-03 22:08:45 +01:00
}//}}}
logout() {//{{{
localStorage.removeItem('session.UUID')
location.href = '/'
2024-12-01 10:21:29 +01:00
}//}}}
2024-11-28 18:11:14 +01:00
}
2024-11-29 09:15:42 +01:00
class Tree extends Component {
constructor(props) {//{{{
super(props)
this.treeNodeComponents = {}
this.treeTrunk = []
this.selectedNode = null
this.expandedNodes = {} // keyed on UUID
2024-12-03 06:53:31 +01:00
this.props.app.tree = this
2024-11-29 09:15:42 +01:00
2024-12-03 22:08:45 +01:00
this.populateFirstLevel()
2024-11-29 09:15:42 +01:00
}//}}}
render({ app }) {//{{{
2024-12-02 15:26:35 +01:00
const renderedTreeTrunk = this.treeTrunk.map(node => {
2024-12-03 22:08:45 +01:00
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} />`
2024-11-29 09:15:42 +01:00
})
2024-12-03 22:08:45 +01:00
return html`
<div id="tree">
2024-12-18 19:12:10 +01:00
<div id="logo" onclick=${() => _notes2.current.goToNode(ROOT_NODE)}><img src="/images/${_VERSION}/logo.svg" /></div>
2024-12-03 22:08:45 +01:00
${renderedTreeTrunk}
</div>`
2024-11-29 09:15:42 +01:00
}//}}}
2024-12-03 22:08:45 +01:00
populateFirstLevel(callback = null) {//{{{
nodeStore.getTreeNodes('', 0)
.then(async res => {
res.sort(Node.sort)
2024-11-29 09:15:42 +01:00
this.treeNodeComponents = {}
this.treeTrunk = []
2024-12-03 22:08:45 +01:00
for (const node of res) {
// The root node isn't supposed to be shown in the tree.
if (node.UUID === '00000000-0000-0000-0000-000000000000')
continue
2024-12-03 22:08:45 +01:00
if (node.ParentUUID === '')
2024-11-29 09:15:42 +01:00
this.treeTrunk.push(node)
2024-12-02 15:26:35 +01:00
}
2024-11-29 09:15:42 +01:00
this.forceUpdate()
if (callback)
callback()
})
.catch(e => { console.log(e); console.log(e.type, e.error); alert(e.error) })
}//}}}
setSelected(node) {//{{{
// 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()
// Expanding selected nodes... I don't know...
this.setNodeExpanded(node.UUID, true)
}//}}}
isSelected(node) {//{{{
return this.selectedNode?.UUID === node.UUID
2024-11-29 09:15:42 +01:00
}//}}}
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)
2024-11-29 09:15:42 +01:00
}
// 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
2024-11-29 09:15:42 +01:00
}//}}}
}
class TreeNode extends Component {
constructor(props) {//{{{
super(props)
2024-12-03 22:08:45 +01:00
this.children_populated = signal(false)
if (this.props.node.Level === 0 || this.props.tree.getNodeExpanded(this.props.node.UUID))
2024-12-03 22:08:45 +01:00
this.fetchChildren()
2024-11-29 09:15:42 +01:00
}//}}}
2024-12-03 22:08:45 +01:00
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))
2024-12-03 22:08:45 +01:00
this.fetchChildren()
2024-11-29 09:15:42 +01:00
2024-12-02 15:26:35 +01:00
const children = node.Children.map(node => {
2024-12-03 22:08:45 +01:00
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} />`
2024-11-29 09:15:42 +01:00
})
let expandImg = ''
2024-12-02 15:26:35 +01:00
if (node.Children.length === 0)
2024-11-29 09:15:42 +01:00
expandImg = html`<img src="/images/${window._VERSION}/leaf.svg" />`
else {
if (tree.getNodeExpanded(node.UUID))
2024-11-29 09:15:42 +01:00
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>
2024-12-18 19:12:10 +01:00
<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>
2024-11-29 09:15:42 +01:00
</div>`
}//}}}
async fetchChildren() {//{{{
await this.props.node.fetchChildren()
this.children_populated.value = true
2024-12-03 22:08:45 +01:00
}//}}}
2024-11-29 09:15:42 +01:00
}
2024-12-02 15:26:35 +01:00
// vim: foldmethod=marker