731 lines
20 KiB
JavaScript
731 lines
20 KiB
JavaScript
import { ROOT_NODE, ORPHANED_NODE , DELETED_NODE} from 'node_store'
|
|
import { Node } from 'node'
|
|
import { CustomHTMLElement } from './lib/custom_html_element.mjs'
|
|
import { Color, Solver } from './lib/css_colorize.mjs'
|
|
|
|
// TreeExpandedHandler is responsible for collapsing or expanding
|
|
// the node tree, wide view or narrow "mobile" view.
|
|
class TreeExpansionHandler {// {{{
|
|
constructor() {
|
|
this.isNarrow = false
|
|
this.initializeMediaHandler()
|
|
this.initializeBusEvents()
|
|
}
|
|
|
|
initializeBusEvents() {
|
|
_mbus.subscribe('TREE_EXPANSION', ({ detail }) => {
|
|
// When a node is selected on the screen and the screen
|
|
// is narrow the tree is automatically hidden.
|
|
//
|
|
// Can't always hide the tree automatically when a node
|
|
// is selected since the wide mode shows the tree as standard.
|
|
if (detail.data?.when == 'narrow' && !this.isNarrow)
|
|
return
|
|
|
|
this.treeExpansion(detail.data?.expand)
|
|
})
|
|
}
|
|
|
|
initializeMediaHandler() {
|
|
const query = window.matchMedia('(max-width: 800px)')
|
|
query.addEventListener('change', event => this.screenNarrowHandler(event))
|
|
|
|
// Run once to set initial state, instead of needing to toggle state.
|
|
this.screenNarrowHandler(query)
|
|
}
|
|
|
|
// When screen becomes narrow, the tree is automatically hidden.
|
|
// Primary purpose is to read content, not browse, which is why
|
|
// the tree is hidden as standard.
|
|
screenNarrowHandler(event) {
|
|
this.isNarrow = event.matches
|
|
|
|
if (this.isNarrow)
|
|
this.treeExpansion(false)
|
|
else
|
|
this.treeExpansion(true)
|
|
}
|
|
|
|
treeExpansion(expanded) {
|
|
const notes2 = document.getElementById('notes2')
|
|
|
|
if (expanded) {
|
|
notes2.classList.remove('hide-tree')
|
|
notes2.classList.add('show-tree')
|
|
} else {
|
|
notes2.classList.add('hide-tree')
|
|
notes2.classList.remove('show-tree')
|
|
}
|
|
}
|
|
}// }}}
|
|
|
|
export class N2Sidebar extends CustomHTMLElement {
|
|
static {// {{{
|
|
this.tmpl = document.createElement('template')
|
|
this.tmpl.innerHTML = `
|
|
<style>
|
|
n2-sidebar {
|
|
#logo {
|
|
display: grid;
|
|
grid-template-columns: 1fr min-content;
|
|
align-items: center;
|
|
justify-items: start;
|
|
cursor: pointer;
|
|
padding: 16px;
|
|
border-bottom: 1px solid var(--line-color);
|
|
|
|
img:first-child {
|
|
height: 24px;
|
|
margin-right: 8px;
|
|
}
|
|
}
|
|
|
|
.icons {
|
|
display: flex;
|
|
justify-content: center;
|
|
margin-top: 16px;
|
|
gap: 8px;
|
|
padding-bottom: 16px;
|
|
border-bottom: 1px solid var(--line-color);
|
|
}
|
|
|
|
|
|
.el-hide-tree {
|
|
font-size: 1.25em;
|
|
font-weight: bold;
|
|
cursor: pointer;
|
|
margin-left: 16px;
|
|
}
|
|
}
|
|
</style>
|
|
<div id="logo">
|
|
<img data-el="logo" src="/images/${_VERSION}/logo.svg" />
|
|
<div data-el="hide-tree"><</div>
|
|
</div>
|
|
<div class="icons">
|
|
<img data-el="sync" class='sync colorize' src="/images/${_VERSION}/icon_refresh.svg" />
|
|
<img data-el="search" class='search colorize' src="/images/${_VERSION}/icon_search.svg" style="height: 22px" />
|
|
<img data-el="settings" class='settings colorize' src="/images/${_VERSION}/icon_settings.svg" />
|
|
</div>
|
|
<div data-el="treenodes"></div>
|
|
`
|
|
}// }}}
|
|
|
|
constructor() {// {{{
|
|
super()
|
|
|
|
this.id = 'tree-nodes'
|
|
this.tabIndex = 0
|
|
|
|
this.treeNodeComponents = {}
|
|
this.expandedNodes = {} // keyed on UUID
|
|
this.selectedNode = null
|
|
this.rendered = false
|
|
|
|
new TreeExpansionHandler()
|
|
|
|
this.addEventListener('keydown', event => this.keyHandler(event))
|
|
this.elSearch.addEventListener('click', () => _mbus.dispatch('op-search'))
|
|
this.elSync.addEventListener('click', () => _sync.run())
|
|
this.elLogo.addEventListener('click', () => _app.goToNode(ROOT_NODE, false, false))
|
|
this.elHideTree.addEventListener('click', event => {
|
|
event.stopPropagation()
|
|
_mbus.dispatch('TREE_EXPANSION', { expand: false })
|
|
})
|
|
|
|
_mbus.subscribe('NODE_MODIFIED', ({ detail }) => {
|
|
const node = detail.data.node
|
|
const treenode = this.treeNodeComponents[node.get('UUID')]
|
|
|
|
if (!treenode)
|
|
return
|
|
|
|
treenode.node = node
|
|
treenode.render(true)
|
|
})
|
|
|
|
/* XXX - set color */
|
|
let color = new Color(0x80, 0x00, 0x33)
|
|
let solver = new Solver(color)
|
|
let result = solver.solve()
|
|
console.log(result.filter)
|
|
}// }}}
|
|
async render() {// {{{
|
|
if (this.rendered)
|
|
alert('Tree should only be rendered once.')
|
|
|
|
this.expandedNodes[ROOT_NODE] = true
|
|
const startnode = await nodeStore.get(ROOT_NODE)
|
|
const starttreenode = new N2TreeNode(this, startnode, null)
|
|
|
|
const deletednode = await nodeStore.get(DELETED_NODE)
|
|
const deletedtreenode = new SpecialNodeDeleted(this, deletednode, null)
|
|
|
|
this.treeNodeComponents[startnode.UUID] = starttreenode
|
|
this.treeNodeComponents[deletednode.UUID] = deletedtreenode
|
|
|
|
this.elTreenodes.appendChild(await starttreenode.render())
|
|
this.elTreenodes.appendChild(await deletedtreenode.render())
|
|
|
|
// Notify the application that the initial tree is rendered (with children)
|
|
// and that initial node selection can take place. App will check URL to
|
|
// select the correct one.
|
|
_mbus.dispatch('TREE_RENDERED')
|
|
|
|
this.rendered = true
|
|
return this
|
|
}// }}}
|
|
reset() {// {{{
|
|
this.treeNodeComponents = {}
|
|
this.rendered = false
|
|
this.elTreenodes.replaceChildren()
|
|
this.render()
|
|
}// }}}
|
|
getNodeExpanded(UUID) {//{{{
|
|
if (this.expandedNodes[UUID] === undefined)
|
|
this.expandedNodes[UUID] = false
|
|
return this.expandedNodes[UUID]
|
|
}//}}}
|
|
setNodeExpanded(node, value) {//{{{
|
|
let expanded = this.expandedNodes[node.UUID]
|
|
|
|
if (expanded === undefined) {
|
|
this.expandedNodes[node.UUID] = false
|
|
expanded = false
|
|
}
|
|
|
|
if (expanded === value)
|
|
return
|
|
|
|
this.expandedNodes[node.UUID] = value
|
|
_mbus.dispatch(`NODE_EXPAND_${node.UUID}`, value)
|
|
}//}}}
|
|
setSelected(node, dontExpand) {//{{{
|
|
if (node === undefined)
|
|
return
|
|
|
|
// 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]?.render(true)
|
|
|
|
// And now the newly selected node is rerendered.
|
|
this.treeNodeComponents[node.UUID]?.render(true)
|
|
|
|
if (!dontExpand)
|
|
this.setNodeExpanded(node, true)
|
|
}//}}}
|
|
isSelected(node) {//{{{
|
|
return this.selectedNode?.UUID === node.UUID
|
|
}//}}}
|
|
getTreeNode(uuid) {// {{{
|
|
return this.treeNodeComponents[uuid]
|
|
}// }}}
|
|
|
|
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':
|
|
if (n.UUID === ROOT_NODE)
|
|
return
|
|
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 || n === undefined || n.UUID == ROOT_NODE)
|
|
return
|
|
|
|
const expanded = this.getNodeExpanded(n.UUID)
|
|
if (expanded && n.hasChildren() && n.UUID !== ROOT_NODE) {
|
|
this.setNodeExpanded(n, false)
|
|
return
|
|
}
|
|
|
|
if (n.isFirstSibling()) {
|
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getParent()?.UUID, dontPush: false, dontExpand: true })
|
|
return
|
|
}
|
|
|
|
const siblingBefore = n.getSiblingBefore()
|
|
const siblingExpanded = this.getNodeExpanded(siblingBefore?.UUID)
|
|
if (siblingBefore !== null && siblingExpanded && siblingBefore.hasChildren()) {
|
|
const siblingAbove = this.getLastExpandedNode(siblingBefore)
|
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: siblingAbove?.UUID, dontPush: false, dontExpand: true })
|
|
return
|
|
}
|
|
|
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getSiblingBefore()?.UUID, dontPush: false, dontExpand: true })
|
|
}//}}}
|
|
async navigateRight(n) {//{{{
|
|
if (n === null || n === undefined)
|
|
return
|
|
|
|
const siblingAfter = n.getSiblingAfter()
|
|
const expanded = this.getNodeExpanded(n.UUID)
|
|
|
|
if (!expanded && n.hasChildren()) {
|
|
this.setNodeExpanded(n, true)
|
|
return
|
|
}
|
|
|
|
if (expanded && n.hasChildren()) {
|
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: n.Children[0]?.UUID, dontPush: false, dontExpand: true })
|
|
return
|
|
}
|
|
|
|
if (n.isLastSibling()) {
|
|
const nextNode = this.getParentWithNextSibling(n)
|
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: nextNode?.UUID, dontPush: false, dontExpand: true })
|
|
return
|
|
}
|
|
|
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getSiblingAfter()?.UUID, dontPush: false, dontExpand: true })
|
|
}//}}}
|
|
async navigateUp(n) {//{{{
|
|
if (n === null || n === undefined || n.UUID == ROOT_NODE)
|
|
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()
|
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: parent?.UUID, dontPush: false, dontExpand: true })
|
|
return
|
|
}
|
|
|
|
if (siblingBefore !== null && siblingExpanded && siblingBefore.hasChildren()) {
|
|
const nodeVisuallyAbove = this.getLastExpandedNode(siblingBefore)
|
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: nodeVisuallyAbove.UUID, dontPush: false, dontExpand: true })
|
|
return
|
|
}
|
|
|
|
if (siblingBefore) {
|
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: siblingBefore.UUID, dontPush: false, dontExpand: true })
|
|
return
|
|
}
|
|
}//}}}
|
|
async navigateDown(n) {//{{{
|
|
if (n === null || n === undefined)
|
|
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)
|
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: wantedNode?.UUID, dontPush: false, dontExpand: true })
|
|
return
|
|
}
|
|
|
|
if (nodeExpanded && n.isLastSibling() && !n.hasChildren()) {
|
|
const wantedNode = this.getParentWithNextSibling(n)
|
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: wantedNode?.UUID, dontPush: false, dontExpand: true })
|
|
return
|
|
}
|
|
|
|
// Node not expanded. Go to this node's next sibling.
|
|
// GoToNode will abort if given null.
|
|
if (!nodeExpanded || !n.hasChildren()) {
|
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getSiblingAfter()?.UUID, dontPush: false, dontExpand: true })
|
|
return
|
|
}
|
|
|
|
// Node is expanded.
|
|
// Children will be visually beneath this node, if any.
|
|
if (nodeExpanded && n.hasChildren()) {
|
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: n.Children[0].UUID, dontPush: false, dontExpand: true })
|
|
return
|
|
}
|
|
}//}}}
|
|
async navigateTop() {//{{{
|
|
const root = await nodeStore.get(ROOT_NODE)
|
|
if (root.Children.length === 0)
|
|
return
|
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: root.Children[0]?.UUID, dontPush: false, dontExpand: 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)
|
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: lastnode?.UUID, dontPush: false, dontExpand: true })
|
|
} else
|
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: root.Children[root.Children.length - 1]?.UUID, dontPush: false, dontExpand: true })
|
|
}//}}}
|
|
|
|
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 makeVisible(node, providedAncestors, dontExpand) {// {{{
|
|
const treenode = this.treeNodeComponents[node.UUID]
|
|
|
|
if (!dontExpand) {
|
|
const ancestors = providedAncestors || await nodeStore.getNodeAncestry(node)
|
|
for (const ancestor of ancestors.reverse()) {
|
|
this.setNodeExpanded(ancestor, true)
|
|
}
|
|
}
|
|
|
|
treenode?.scrollIntoView({ block: 'nearest' })
|
|
}// }}}
|
|
}
|
|
|
|
export class N2TreeNode extends CustomHTMLElement {
|
|
static DRAG_ICON = new Image()
|
|
static DRAG_ICON_OK = new Image()
|
|
|
|
static {// {{{
|
|
N2TreeNode.DRAG_ICON.src = `/images/${_VERSION}/leaf.svg`
|
|
N2TreeNode.DRAG_ICON_OK.src = `/images/${_VERSION}/expanded.svg`
|
|
|
|
this.tmpl = document.createElement('template')
|
|
this.tmpl.innerHTML = `
|
|
<style>
|
|
n2-sidebar:focus-within {
|
|
& > .el-name {
|
|
&.selected {
|
|
span {
|
|
position:relative;
|
|
}
|
|
span:before {
|
|
position: absolute;
|
|
content: '';
|
|
top: -8px;
|
|
left: -36px;
|
|
right: -8px;
|
|
bottom: -4px;
|
|
z-index: -1;
|
|
|
|
background-color: #f8f8f8;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
n2-treenode {
|
|
& > .el-name {
|
|
white-space: nowrap;
|
|
width: min-content;
|
|
}
|
|
|
|
&.drag-source {
|
|
& > .el-name {
|
|
position: relative;
|
|
}
|
|
|
|
& > .el-name:after {
|
|
position: absolute;
|
|
content: url('/images/${_VERSION}/icon_drag_source.svg');
|
|
filter: var(--colorize);
|
|
top: -1px;
|
|
right: -24px;
|
|
}
|
|
}
|
|
|
|
&.drag-target {
|
|
position: relative;
|
|
|
|
& > .el-name {
|
|
anchor-name: --name;
|
|
}
|
|
|
|
& > .el-name:after {
|
|
content: '';
|
|
position: absolute;
|
|
|
|
border: 2px dashed #888;
|
|
|
|
top: calc(anchor(--name top) - 12px);
|
|
right: calc(anchor(--name right) - 8px);
|
|
bottom: calc(anchor(--name bottom) - 8px);
|
|
left: calc(anchor(--name left) - 40px);
|
|
|
|
pointer-events: none;
|
|
}
|
|
|
|
& > .el-drag-icon {
|
|
display: block;
|
|
top: 0px;
|
|
left: 0px;
|
|
z-index: 16384;
|
|
}
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<div data-el="expand-toggle" class="expand-toggle">
|
|
<img data-el="expand" draggable="false">
|
|
</div>
|
|
<div data-el="name" class="name"><span></span></div>
|
|
<div data-el="children" class="children"></div>
|
|
`
|
|
}// }}}
|
|
|
|
constructor(sidebar, node, parent) {//{{{
|
|
super()
|
|
this.setAttribute('draggable', 'true')
|
|
this.classList.add('node')
|
|
|
|
this.sidebar = sidebar
|
|
this.node = node
|
|
this.parent = parent
|
|
|
|
this.children_populated = false
|
|
this.rendered = false
|
|
this.dragNode = null
|
|
|
|
this.elExpandToggle.addEventListener('click', () => this.sidebar.setNodeExpanded(this.node, !this.sidebar.getNodeExpanded(this.node.UUID)))
|
|
this.elName.addEventListener('click', () => _mbus.dispatch('TREE_NODE_SELECTED', this.node))
|
|
|
|
_mbus.subscribe(`NODE_EXPAND_${node.UUID}`, _state => {
|
|
this.render(true)
|
|
})
|
|
|
|
// Drag-and-dropping of nodes
|
|
this.addEventListener('dragstart', event => this.dragStart(event))
|
|
this.addEventListener('dragend', event => this.dragEnd(event))
|
|
this.addEventListener('dragover', event => this.dragOver(event))
|
|
this.addEventListener('drop', event => this.dragDrop(event))
|
|
this.elName.addEventListener('dragenter', event => this.dragEnter(event))
|
|
this.elName.addEventListener('dragleave', event => this.dragLeave(event))
|
|
}// }}}
|
|
dragStart(e) {// {{{
|
|
if (this.node.isModified()) {
|
|
alert('Save note before moving it.')
|
|
e.stopPropagation()
|
|
e.preventDefault()
|
|
return
|
|
}
|
|
|
|
this.classList.add('drag-source')
|
|
const blankPixel = new Image()
|
|
blankPixel.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
|
|
e.dataTransfer.setDragImage(blankPixel, 0, 0)
|
|
e.dataTransfer.allowedEffects = 'none'
|
|
e.stopPropagation()
|
|
_app.dragIcon.setSource(this)
|
|
_app.dragIcon.start()
|
|
}// }}}
|
|
dragEnd(e) {// {{{
|
|
this.classList.remove('drag-source')
|
|
_app.dragIcon.end()
|
|
e.stopPropagation()
|
|
}// }}}
|
|
dragOver(e) {// {{{
|
|
e.dataTransfer.dropEffect = 'move'
|
|
e.preventDefault()
|
|
}// }}}
|
|
async dragDrop(e) {// {{{
|
|
try {
|
|
e.stopPropagation()
|
|
const sourceNode = _app.dragIcon.getSource()
|
|
|
|
// Abort if user drops the node back on itself.
|
|
if (sourceNode.node.UUID === this.node.UUID)
|
|
return
|
|
|
|
await _app.moveNode(sourceNode.node, this.node.UUID)
|
|
|
|
_app.sidebar.setNodeExpanded(this, true)
|
|
await this.render(true, true)
|
|
await sourceNode.render(true, true)
|
|
} catch (e) {
|
|
console.error(e)
|
|
alert(e)
|
|
} finally {
|
|
this.dragLeave(e)
|
|
}
|
|
}// }}}
|
|
dragEnter(e) {// {{{
|
|
const targetNode = e.target.closest('n2-treenode')
|
|
if (targetNode.classList.contains('drag-source'))
|
|
return
|
|
e.stopPropagation()
|
|
_app.dragIcon.icon('ok')
|
|
this.classList.add('drag-target')
|
|
}// }}}
|
|
dragLeave(e) {// {{{
|
|
e.stopPropagation()
|
|
e.dataTransfer.dropEffect = 'none'
|
|
e.dataTransfer.setDragImage(N2TreeNode.DRAG_ICON, -16, 8)
|
|
_app.dragIcon.icon('')
|
|
this.classList.remove('drag-target')
|
|
}// }}}
|
|
async fetchChildren(force_fetch) {//{{{
|
|
if (this.children_populated && !force_fetch)
|
|
return
|
|
|
|
await this.node.fetchChildren()
|
|
this.children_populated = true
|
|
}//}}}
|
|
async render(force_update, force_refetch_children) {//{{{
|
|
if (this.rendered && force_update !== true)
|
|
return this
|
|
|
|
if (this.sidebar.getNodeExpanded(this.node.UUID) || force_refetch_children)
|
|
await this.fetchChildren(force_refetch_children)
|
|
|
|
// Update the name and selected status.
|
|
this.elName.querySelector('span').innerText = this.node.get('Name')
|
|
|
|
if (this.sidebar.isSelected(this.node))
|
|
this.elName.classList.add('selected')
|
|
else
|
|
this.elName.classList.remove('selected')
|
|
|
|
// Update expansion state
|
|
const expanded = this.node.hasChildren() && this.sidebar.getNodeExpanded(this.node.UUID)
|
|
if (expanded) {
|
|
this.elChildren.classList.add('expanded')
|
|
this.elChildren.classList.remove('collapsed')
|
|
} else {
|
|
this.elChildren.classList.remove('expanded')
|
|
this.elChildren.classList.add('collapsed')
|
|
}
|
|
|
|
// The expand icon <img> is only changed to not get a flickering when re-rendering.
|
|
if (this.node.UUID === ROOT_NODE)
|
|
this.setImgSrc(this.elExpand, `/images/${window._VERSION}/icon_home.svg`)
|
|
|
|
else if (this.node.UUID === '00000000-0000-0000-0000-000000000002') {
|
|
this.setImgSrc(this.elExpand, `/images/${window._VERSION}/leaf_deleted.svg`)
|
|
this.elExpand.classList.add('deleted')
|
|
}
|
|
|
|
else if (!this.node.hasChildren())
|
|
this.setImgSrc(this.elExpand, `/images/${window._VERSION}/leaf.svg`)
|
|
else if (this.sidebar.getNodeExpanded(this.node.UUID))
|
|
this.setImgSrc(this.elExpand, `/images/${window._VERSION}/expanded.svg`)
|
|
else
|
|
this.setImgSrc(this.elExpand, `/images/${window._VERSION}/collapsed.svg`)
|
|
|
|
// Should children be rendered?
|
|
let children = []
|
|
if (expanded)
|
|
children = this.node.Children.map(node => {
|
|
let treenode = this.sidebar.treeNodeComponents[node.UUID]
|
|
if (treenode === undefined) {
|
|
treenode = new N2TreeNode(this.sidebar, node, this)
|
|
this.sidebar.treeNodeComponents[node.UUID] = treenode
|
|
}
|
|
return treenode
|
|
})
|
|
|
|
const renderedChildren = []
|
|
for (const c of children)
|
|
renderedChildren.push(await c.render())
|
|
this.elChildren.replaceChildren(...renderedChildren)
|
|
|
|
this.rendered = true
|
|
return this
|
|
}//}}}
|
|
|
|
setImgSrc(img, newSrc) {// {{{
|
|
if (img.getAttribute('src') === newSrc)
|
|
return
|
|
img.setAttribute('src', newSrc)
|
|
}// }}}
|
|
}
|
|
|
|
class SpecialNodeDeleted extends N2TreeNode {
|
|
constructor(sidebar, node, parent) {//{{{
|
|
super(sidebar, node, parent)
|
|
this.removeAttribute('draggable')
|
|
}//}}}
|
|
}
|
|
|
|
customElements.define('n2-sidebar', N2Sidebar)
|
|
customElements.define('n2-treenode', N2TreeNode)
|
|
customElements.define('n2-specialnodedeleted', SpecialNodeDeleted)
|
|
|
|
// vim: foldmethod=marker
|