Tree render and navigation with note rendering
This commit is contained in:
parent
dd27be67b9
commit
1ce8e29e37
7 changed files with 515 additions and 62 deletions
|
|
@ -17,15 +17,10 @@ html {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
#tree-native {
|
|
||||||
grid-area: tree;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
#tree {
|
#tree {
|
||||||
grid-area: tree;
|
grid-area: tree;
|
||||||
padding: 16px 32px;
|
display: grid;
|
||||||
background-color: #333;
|
padding: 16px 0px 16px 16px;
|
||||||
color: #ddd;
|
color: #ddd;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
border-left: 2px solid #333;
|
border-left: 2px solid #333;
|
||||||
|
|
@ -37,9 +32,11 @@ html {
|
||||||
display: grid;
|
display: grid;
|
||||||
position: relative;
|
position: relative;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
|
margin-top: 8px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
margin-left: 24px;
|
margin-left: 24px;
|
||||||
margin-right: 24px;
|
margin-right: 24px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
#tree #logo img {
|
#tree #logo img {
|
||||||
width: 128px;
|
width: 128px;
|
||||||
|
|
@ -85,12 +82,18 @@ html {
|
||||||
#tree .node .children.collapsed {
|
#tree .node .children.collapsed {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
#tree-nodes {
|
||||||
|
padding: 16px 32px;
|
||||||
|
background-color: #333;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 5px 5px 10px -5px rgba(0, 0, 0, 0.75);
|
||||||
|
}
|
||||||
#crumbs {
|
#crumbs {
|
||||||
grid-area: crumbs;
|
grid-area: crumbs;
|
||||||
display: grid;
|
display: grid;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
margin: 0px 16px;
|
margin: 16px 16px;
|
||||||
}
|
}
|
||||||
#crumbs .crumbs {
|
#crumbs .crumbs {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -114,6 +117,10 @@ html {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
|
#crumbs .crumbs .crumb a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
#crumbs .crumbs .crumb:after {
|
#crumbs .crumbs .crumb:after {
|
||||||
content: "•";
|
content: "•";
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
|
|
|
||||||
298
static/js/app.mjs
Normal file
298
static/js/app.mjs
Normal file
|
|
@ -0,0 +1,298 @@
|
||||||
|
import { ROOT_NODE } from 'node_store'
|
||||||
|
import { TreeNative } from 'tree'
|
||||||
|
import { NodeUINative } from 'node'
|
||||||
|
|
||||||
|
export class App {
|
||||||
|
constructor() {// {{{
|
||||||
|
this.currentNode = null
|
||||||
|
this.treeNative = new TreeNative()
|
||||||
|
this.crumbs = new Crumbs()
|
||||||
|
this.crumbsElement = document.getElementById('crumbs')
|
||||||
|
this.nodeUI = new NodeUINative(document.getElementById('note'))
|
||||||
|
|
||||||
|
_mbus.subscribe('TREE_TRUNK_FETCHED', async () => {
|
||||||
|
document.getElementById('tree').append(this.treeNative.render())
|
||||||
|
document.getElementById('tree-nodes')?.focus()
|
||||||
|
|
||||||
|
const startNode = await this.getStartNode()
|
||||||
|
this.goToNode(startNode.UUID, false, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
_mbus.subscribe('TREE_NODE_SELECTED', event => {
|
||||||
|
const node = event.detail.data
|
||||||
|
this.goToNode(node.UUID, false, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
_mbus.subscribe('GO_TO_NODE', event => {
|
||||||
|
const node = event.detail.data
|
||||||
|
this.goToNode(node.nodeUUID, node.dontPush, node.dontExpand)
|
||||||
|
})
|
||||||
|
|
||||||
|
window.addEventListener('keydown', event => this.keyHandler(event))
|
||||||
|
|
||||||
|
window._sync = new Sync()
|
||||||
|
window._sync.run()
|
||||||
|
}// }}}
|
||||||
|
|
||||||
|
keyHandler(event) {//{{{
|
||||||
|
let handled = true
|
||||||
|
|
||||||
|
// All keybindings is Alt+Shift, since the popular browsers at the time (2023) allows to override thees.
|
||||||
|
// Ctrl+S is the exception to using Alt+Shift, since it is overridable and in such widespread use for saving.
|
||||||
|
// Thus, the exception is acceptable to consequent use of alt+shift.
|
||||||
|
if (!(event.shiftKey && event.altKey) && !(event.key.toUpperCase() === 'S' && event.ctrlKey))
|
||||||
|
return
|
||||||
|
|
||||||
|
switch (event.key.toUpperCase()) {
|
||||||
|
case 'T':
|
||||||
|
console.log(document.activeElement.id)
|
||||||
|
if (document.activeElement.id === 'tree-nodes')
|
||||||
|
document.getElementById('node-content').focus()
|
||||||
|
else
|
||||||
|
document.getElementById('tree-nodes').focus()
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'F':
|
||||||
|
_mbus.dispatch('op-search')
|
||||||
|
break
|
||||||
|
/*
|
||||||
|
case 'C':
|
||||||
|
this.showPage('node')
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'E':
|
||||||
|
this.showPage('keys')
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'M':
|
||||||
|
this.toggleMarkdown()
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'N':
|
||||||
|
this.createNode()
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'P':
|
||||||
|
this.showPage('node-properties')
|
||||||
|
break
|
||||||
|
|
||||||
|
*/
|
||||||
|
case 'S':
|
||||||
|
this.saveNode()
|
||||||
|
/*
|
||||||
|
else if (this.page.value === 'node-properties')
|
||||||
|
this.nodeProperties.current.save()
|
||||||
|
*/
|
||||||
|
break
|
||||||
|
/*
|
||||||
|
|
||||||
|
case 'U':
|
||||||
|
this.showPage('upload')
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'F':
|
||||||
|
this.showPage('search')
|
||||||
|
break
|
||||||
|
*/
|
||||||
|
|
||||||
|
default:
|
||||||
|
handled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handled) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
}
|
||||||
|
}//}}}
|
||||||
|
async 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]
|
||||||
|
|
||||||
|
return await nodeStore.get(nodeUUID)
|
||||||
|
}//}}}
|
||||||
|
async saveNode() {//{{{
|
||||||
|
if (!this.currentNode.isModified())
|
||||||
|
return
|
||||||
|
|
||||||
|
/* The node history is a local store for node history.
|
||||||
|
* This could be provisioned from the server or cleared if
|
||||||
|
* deemed unnecessary.
|
||||||
|
*
|
||||||
|
* The send queue is what will be sent back to the server
|
||||||
|
* to have a recorded history of the notes.
|
||||||
|
*
|
||||||
|
* A setting to be implemented in the future could be to
|
||||||
|
* not save the history locally at all. */
|
||||||
|
const node = this.currentNode
|
||||||
|
|
||||||
|
// The node is still in its old state and will present
|
||||||
|
// the unmodified content to the node store.
|
||||||
|
const history = nodeStore.nodesHistory.add(node)
|
||||||
|
|
||||||
|
// Prepares the node object for saving.
|
||||||
|
// Sets Updated value to current date and time.
|
||||||
|
await node.save()
|
||||||
|
|
||||||
|
// Updated node is added to the send queue to be stored on server.
|
||||||
|
const sendQueue = nodeStore.sendQueue.add(node)
|
||||||
|
|
||||||
|
// Updated node is saved to the primary node store.
|
||||||
|
const nodeStoreAdding = nodeStore.add([node])
|
||||||
|
|
||||||
|
await Promise.all([history, sendQueue, nodeStoreAdding])
|
||||||
|
}//}}}
|
||||||
|
async goToNode(nodeUUID, dontPush, dontExpand) {//{{{
|
||||||
|
if (nodeUUID === null || nodeUUID === undefined)
|
||||||
|
return
|
||||||
|
|
||||||
|
// Don't switch notes until saved.
|
||||||
|
if (this.nodeUI.isModified()) {
|
||||||
|
if (!confirm("Changes not saved. Do you want to discard changes?"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dontPush)
|
||||||
|
history.pushState({ nodeUUID }, '', `/notes2#${nodeUUID}`)
|
||||||
|
|
||||||
|
const node = nodeStore.node(nodeUUID)
|
||||||
|
|
||||||
|
node.reset() // any modifications are discarded.
|
||||||
|
|
||||||
|
this.currentNode = node
|
||||||
|
this.treeNative.setSelected(node, dontExpand)
|
||||||
|
|
||||||
|
const ancestors = await nodeStore.getNodeAncestry(node)
|
||||||
|
_mbus.dispatch('CRUMBS_SET', ancestors, () => this.crumbsElement.replaceChildren(this.crumbs.render()))
|
||||||
|
_mbus.dispatch('NODE_UI_OPEN', node)
|
||||||
|
_mbus.dispatch('NODE_UNMODIFIED')
|
||||||
|
|
||||||
|
// Scrolls node into view.
|
||||||
|
this.treeNative.makeVisible(node)
|
||||||
|
}//}}}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Crumbs {
|
||||||
|
constructor() {// {{{
|
||||||
|
this.crumbs = []
|
||||||
|
this.crumbsDiv = document.createElement('div')
|
||||||
|
this.crumbsDiv.classList.add('crumbs')
|
||||||
|
|
||||||
|
_mbus.subscribe('CRUMBS_SET', event => {
|
||||||
|
this.crumbs = event.detail.data
|
||||||
|
})
|
||||||
|
}// }}}
|
||||||
|
render() {// {{{
|
||||||
|
const crumbs = this.crumbs.map(node =>
|
||||||
|
(new Crumb(
|
||||||
|
node.get('Name'),
|
||||||
|
node.UUID,
|
||||||
|
)).render()
|
||||||
|
)
|
||||||
|
|
||||||
|
const start = (new Crumb('Start', ROOT_NODE)).render()
|
||||||
|
crumbs.push(start)
|
||||||
|
|
||||||
|
this.crumbsDiv.replaceChildren(...crumbs.reverse())
|
||||||
|
return this.crumbsDiv
|
||||||
|
}// }}}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Crumb {
|
||||||
|
constructor(label, uuid) {// {{{
|
||||||
|
this.label = label
|
||||||
|
this.uuid = uuid
|
||||||
|
}// }}}
|
||||||
|
render() {// {{{
|
||||||
|
const crumb = document.createElement('div')
|
||||||
|
crumb.classList.add('crumb')
|
||||||
|
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = `/notes2#${this.uuid}`
|
||||||
|
link.innerText = this.label
|
||||||
|
|
||||||
|
crumb.appendChild(link)
|
||||||
|
return crumb
|
||||||
|
|
||||||
|
}// }}}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Op {
|
||||||
|
constructor(id) {
|
||||||
|
this.id = id
|
||||||
|
_mbus.subscribe(this.id, p => this.render(p))
|
||||||
|
}
|
||||||
|
render(html) {
|
||||||
|
const op = document.getElementById('op')
|
||||||
|
const t = document.createElement('template')
|
||||||
|
t.innerHTML = `<dialog id="${this.id}" class="op">${html}</dialog>`
|
||||||
|
op.replaceChildren(t.content)
|
||||||
|
document.getElementById(this.id).showModal()
|
||||||
|
}
|
||||||
|
get(selector) {
|
||||||
|
return document.querySelector(`#${this.id} ${selector}`)
|
||||||
|
}
|
||||||
|
bind(selector, event, fn) {
|
||||||
|
this.get(selector).addEventListener(event, evt => fn(evt))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function tmpl(html) {
|
||||||
|
const el = document.createElement('template')
|
||||||
|
el.innerHTML = html
|
||||||
|
return el.content.children
|
||||||
|
}
|
||||||
|
|
||||||
|
class OpSearch extends Op {
|
||||||
|
constructor() {
|
||||||
|
super('op-search')
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
super.render(`
|
||||||
|
<div class="header">Search</div>
|
||||||
|
<div>
|
||||||
|
<input type="text" />
|
||||||
|
</div>
|
||||||
|
<div class="header">Results</div>
|
||||||
|
<div class="results"></div>
|
||||||
|
`)
|
||||||
|
|
||||||
|
this.bind('input[type="text"]', 'keydown', evt => this.search(evt))
|
||||||
|
}
|
||||||
|
|
||||||
|
search(event) {
|
||||||
|
if (event.key !== 'Enter')
|
||||||
|
return
|
||||||
|
|
||||||
|
const searchFor = document.querySelector('#op-search input').value
|
||||||
|
nodeStore.search(searchFor, ROOT_NODE)
|
||||||
|
.then(res => this.displayResults(res))
|
||||||
|
}
|
||||||
|
|
||||||
|
displayResults(results) {
|
||||||
|
const rs = []
|
||||||
|
for (const r of results) {
|
||||||
|
const ancestors = r.ancestry.reverse().map(a => {
|
||||||
|
const div = tmpl(`<div class="ancestor">${a.data.Name}</div>`)
|
||||||
|
div[0].addEventListener('click', ()=>_notes2.current.goToNode(a.UUID))
|
||||||
|
return div[0]
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const div = tmpl(`<div>${r.name}</div>`)
|
||||||
|
div[0].addEventListener('click', ()=>_notes2.current.goToNode(r.uuid))
|
||||||
|
rs.push(...div)
|
||||||
|
|
||||||
|
const ancDev = tmpl('<div class="ancestors"></div>')
|
||||||
|
ancDev[0].append(...ancestors)
|
||||||
|
rs.push(ancDev[0])
|
||||||
|
}
|
||||||
|
this.get('.results').replaceChildren(...rs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// vim: foldmethod=marker
|
||||||
|
|
@ -1,17 +1,48 @@
|
||||||
export class MessageBus {
|
export class MessageBus {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
this.log = false
|
||||||
this.bus = new EventTarget()
|
this.bus = new EventTarget()
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe(eventName, fn) {
|
subscribe(eventName, fn) {
|
||||||
this.bus.addEventListener(eventName, fn)
|
if (this.log) {
|
||||||
|
console.groupCollapsed('MBUS subscribe - ', eventName);
|
||||||
|
console.trace(); // hidden in collapsed group
|
||||||
|
console.groupEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bus.addEventListener(eventName, event=>{
|
||||||
|
fn(event)
|
||||||
|
if (event.detail.callback !== undefined)
|
||||||
|
event.detail.callback(event)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
unsubscribe(eventName, fn) {
|
unsubscribe(eventName, fn) {
|
||||||
|
if (this.log) {
|
||||||
|
console.groupCollapsed('MBUS unsubscribe - ', eventName);
|
||||||
|
console.trace(); // hidden in collapsed group
|
||||||
|
console.groupEnd();
|
||||||
|
}
|
||||||
|
|
||||||
this.bus.removeEventListener(eventName, fn)
|
this.bus.removeEventListener(eventName, fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(eventName, data) {
|
dispatch(eventName, data, callback) {
|
||||||
this.bus.dispatchEvent(new CustomEvent(eventName, { detail: data }))
|
if (this.log) {
|
||||||
|
console.groupCollapsed('MBUS dispatch - ', eventName);
|
||||||
|
console.log('data', data);
|
||||||
|
console.trace(); // hidden in collapsed group
|
||||||
|
console.groupEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = new CustomEvent(eventName, {
|
||||||
|
detail: {
|
||||||
|
data,
|
||||||
|
callback,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.bus.dispatchEvent(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -331,6 +331,61 @@ class NodeContent extends Component {
|
||||||
}//}}}
|
}//}}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class NodeUINative {
|
||||||
|
constructor(parentElement) {// {{{
|
||||||
|
this.node = null
|
||||||
|
this.parent = parentElement
|
||||||
|
this.parent.replaceChildren(this.createElements())
|
||||||
|
|
||||||
|
_mbus.subscribe('NODE_UI_OPEN', event => {
|
||||||
|
this.node = event.detail.data
|
||||||
|
this.render()
|
||||||
|
})
|
||||||
|
|
||||||
|
_mbus.subscribe('NODE_MODIFIED', ()=>{
|
||||||
|
document.querySelector('#crumbs .crumbs')?.classList.add('node-modified')
|
||||||
|
})
|
||||||
|
|
||||||
|
_mbus.subscribe('NODE_UNMODIFIED', ()=>{
|
||||||
|
document.querySelector('#crumbs .crumbs')?.classList.remove('node-modified')
|
||||||
|
})
|
||||||
|
}// }}}
|
||||||
|
createElements() {// {{{
|
||||||
|
const tmpl = document.createElement('template')
|
||||||
|
tmpl.innerHTML = `
|
||||||
|
<div id="name"></div>
|
||||||
|
<div class="grow-wrap" style="display: none">
|
||||||
|
<textarea id="node-content" class="node-content" required rows=1></textarea>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
tmpl.content.querySelector('#node-content').addEventListener('input', event=>this.contentChanged(event))
|
||||||
|
|
||||||
|
return tmpl.content
|
||||||
|
}// }}}
|
||||||
|
render() {// {{{
|
||||||
|
this.parent.querySelector('.grow-wrap').style.display = (this.node === null ? 'none' : 'grid')
|
||||||
|
this.parent.querySelector('#name').innerText = this.node?.get('Name') ?? ''
|
||||||
|
this.parent.querySelector('#node-content').value = this.node?.get('Content') ?? ''
|
||||||
|
|
||||||
|
this.resize()
|
||||||
|
return this.parent
|
||||||
|
}// }}}
|
||||||
|
|
||||||
|
resize() {//{{{
|
||||||
|
const textarea = this.parent.querySelector('#node-content')
|
||||||
|
textarea.parentNode.dataset.replicatedValue = textarea.value
|
||||||
|
}//}}}
|
||||||
|
contentChanged(event) {//{{{
|
||||||
|
this.node.setContent(event.target.value)
|
||||||
|
this.resize()
|
||||||
|
}//}}}
|
||||||
|
isModified() {// {{{
|
||||||
|
return this.node?.isModified()
|
||||||
|
}// }}}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
export class Node {
|
export class Node {
|
||||||
static sort(a, b) {//{{{
|
static sort(a, b) {//{{{
|
||||||
if (a.data.Name < b.data.Name) return -1
|
if (a.data.Name < b.data.Name) return -1
|
||||||
|
|
@ -340,7 +395,6 @@ export class Node {
|
||||||
constructor(nodeData, level) {//{{{
|
constructor(nodeData, level) {//{{{
|
||||||
this.Level = level
|
this.Level = level
|
||||||
this.data = nodeData
|
this.data = nodeData
|
||||||
|
|
||||||
this.UUID = nodeData.UUID
|
this.UUID = nodeData.UUID
|
||||||
|
|
||||||
// Toplevel nodes are normalized to have the ROOT_NODE as parent.
|
// Toplevel nodes are normalized to have the ROOT_NODE as parent.
|
||||||
|
|
@ -354,13 +408,12 @@ export class Node {
|
||||||
this.Children = []
|
this.Children = []
|
||||||
this.Ancestors = []
|
this.Ancestors = []
|
||||||
|
|
||||||
this._content = this.data.Content
|
|
||||||
this._modified = false
|
|
||||||
|
|
||||||
this._sibling_before = null
|
this._sibling_before = null
|
||||||
this._sibling_after = null
|
this._sibling_after = null
|
||||||
this._parent = null
|
this._parent = null
|
||||||
|
|
||||||
|
this.reset()
|
||||||
|
|
||||||
/*
|
/*
|
||||||
this.RenderMarkdown = signal(nodeData.RenderMarkdown)
|
this.RenderMarkdown = signal(nodeData.RenderMarkdown)
|
||||||
this.Markdown = false
|
this.Markdown = false
|
||||||
|
|
@ -377,6 +430,10 @@ export class Node {
|
||||||
*/
|
*/
|
||||||
}//}}}
|
}//}}}
|
||||||
|
|
||||||
|
reset() {// {{{
|
||||||
|
this._content = this.data.Content
|
||||||
|
this._modified = false
|
||||||
|
}// }}}
|
||||||
get(prop) {//{{{
|
get(prop) {//{{{
|
||||||
return this.data[prop]
|
return this.data[prop]
|
||||||
}//}}}
|
}//}}}
|
||||||
|
|
@ -384,6 +441,9 @@ export class Node {
|
||||||
// '2024-12-17T17:33:48.85939Z
|
// '2024-12-17T17:33:48.85939Z
|
||||||
return new Date(Date.parse(this.data.Updated))
|
return new Date(Date.parse(this.data.Updated))
|
||||||
}//}}}
|
}//}}}
|
||||||
|
isModified() {// {{{
|
||||||
|
return this._modified
|
||||||
|
}// }}}
|
||||||
hasFetchedChildren() {//{{{
|
hasFetchedChildren() {//{{{
|
||||||
return this._children_fetched
|
return this._children_fetched
|
||||||
}//}}}
|
}//}}}
|
||||||
|
|
@ -436,12 +496,12 @@ export class Node {
|
||||||
if (this.CryptoKeyID != 0 && !this._decrypted)
|
if (this.CryptoKeyID != 0 && !this._decrypted)
|
||||||
this.#decrypt()
|
this.#decrypt()
|
||||||
*/
|
*/
|
||||||
this.modified = true
|
|
||||||
return this._content
|
return this._content
|
||||||
}//}}}
|
}//}}}
|
||||||
|
|
||||||
setContent(new_content) {//{{{
|
setContent(new_content) {//{{{
|
||||||
this._content = new_content
|
this._content = new_content
|
||||||
|
this._modified = true
|
||||||
|
_mbus.dispatch('NODE_MODIFIED')
|
||||||
/* TODO - implement crypto
|
/* TODO - implement crypto
|
||||||
if (this.CryptoKeyID == 0)
|
if (this.CryptoKeyID == 0)
|
||||||
// Logic behind plaintext not being decrypted is that
|
// Logic behind plaintext not being decrypted is that
|
||||||
|
|
@ -456,6 +516,8 @@ export class Node {
|
||||||
this.data.Updated = new Date().toISOString()
|
this.data.Updated = new Date().toISOString()
|
||||||
this._modified = false
|
this._modified = false
|
||||||
|
|
||||||
|
_mbus.dispatch('NODE_UNMODIFIED')
|
||||||
|
|
||||||
// When stored into database and ancestry was changed,
|
// When stored into database and ancestry was changed,
|
||||||
// the ancestry path could be interesting.
|
// the ancestry path could be interesting.
|
||||||
const ancestors = await nodeStore.getNodeAncestry(this)
|
const ancestors = await nodeStore.getNodeAncestry(this)
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,6 @@ export class TreeNative {
|
||||||
this.selectedNode = null
|
this.selectedNode = null
|
||||||
this.rendered = false
|
this.rendered = false
|
||||||
|
|
||||||
window._mbus.subscribe('TREE_TRUNK_FETCHED', ()=>{
|
|
||||||
document.getElementById('tree').append(this.render())
|
|
||||||
})
|
|
||||||
|
|
||||||
this.populateFirstLevel()
|
this.populateFirstLevel()
|
||||||
}// }}}
|
}// }}}
|
||||||
render() {// {{{
|
render() {// {{{
|
||||||
|
|
@ -19,10 +15,25 @@ export class TreeNative {
|
||||||
alert('Tree should only be rendered once.')
|
alert('Tree should only be rendered once.')
|
||||||
|
|
||||||
const tmpl = document.createElement('template')
|
const tmpl = document.createElement('template')
|
||||||
tmpl.innerHTML = '<div id="tree-nodes" tabindex=0><div>'
|
tmpl.innerHTML = `
|
||||||
|
<div id="tree-nodes" tabindex=0>
|
||||||
|
<div id="logo"><img src="/images/${_VERSION}/logo.svg" /></div>
|
||||||
|
<div class="icons">
|
||||||
|
<img src="/images/${_VERSION}/icon_search.svg" style="height: 22px" />
|
||||||
|
<img src="/images/${_VERSION}/icon_refresh.svg" />
|
||||||
|
</div>
|
||||||
|
<div>`
|
||||||
|
|
||||||
|
/*
|
||||||
|
onclick=${() => _mbus.dispatch('op-search')}
|
||||||
|
onclick=${() => _sync.run()}
|
||||||
|
*/
|
||||||
|
|
||||||
const treeEl = tmpl.content.getElementById('tree-nodes')
|
const treeEl = tmpl.content.getElementById('tree-nodes')
|
||||||
treeEl.addEventListener('keydown', event=>this.keyHandler(event))
|
treeEl.addEventListener('keydown', event=>this.keyHandler(event))
|
||||||
|
|
||||||
|
tmpl.content.getElementById('logo').addEventListener('click', ()=>_app.goToNode(ROOT_NODE, false, false))
|
||||||
|
|
||||||
for (const node of this.treeTrunk) {
|
for (const node of this.treeTrunk) {
|
||||||
const treenode = new TreeNodeNative(this, node)
|
const treenode = new TreeNodeNative(this, node)
|
||||||
this.treeNodeComponents[node.UUID] = treenode
|
this.treeNodeComponents[node.UUID] = treenode
|
||||||
|
|
@ -57,12 +68,23 @@ export class TreeNative {
|
||||||
return this.expandedNodes[UUID]
|
return this.expandedNodes[UUID]
|
||||||
}//}}}
|
}//}}}
|
||||||
setNodeExpanded(node, value) {//{{{
|
setNodeExpanded(node, value) {//{{{
|
||||||
// Creating a default value if it doesn't exist already.
|
let expanded = this.expandedNodes[node.UUID]
|
||||||
this.getNodeExpanded(node.UUID)
|
|
||||||
|
if (expanded === undefined) {
|
||||||
|
this.expandedNodes[node.UUID] = false
|
||||||
|
expanded = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expanded === value)
|
||||||
|
return
|
||||||
|
|
||||||
this.expandedNodes[node.UUID] = value
|
this.expandedNodes[node.UUID] = value
|
||||||
_mbus.dispatch(`NODE_EXPAND_${node.UUID}`, value)
|
_mbus.dispatch(`NODE_EXPAND_${node.UUID}`, value)
|
||||||
}//}}}
|
}//}}}
|
||||||
setSelected(node, dontExpand) {//{{{
|
setSelected(node, dontExpand) {//{{{
|
||||||
|
if (node === undefined)
|
||||||
|
return
|
||||||
|
|
||||||
// The previously selected node, if any, needs to be rerendered
|
// The previously selected node, if any, needs to be rerendered
|
||||||
// to not retain its 'selected' class.
|
// to not retain its 'selected' class.
|
||||||
const prevUUID = this.selectedNode?.UUID
|
const prevUUID = this.selectedNode?.UUID
|
||||||
|
|
@ -143,7 +165,7 @@ export class TreeNative {
|
||||||
}
|
}
|
||||||
}//}}}
|
}//}}}
|
||||||
async navigateLeft(n) {//{{{
|
async navigateLeft(n) {//{{{
|
||||||
if (n === null)
|
if (n === null || n === undefined)
|
||||||
return
|
return
|
||||||
|
|
||||||
const expanded = this.getNodeExpanded(n.UUID)
|
const expanded = this.getNodeExpanded(n.UUID)
|
||||||
|
|
@ -153,7 +175,7 @@ export class TreeNative {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (n.isFirstSibling() && n.getParent().UUID !== ROOT_NODE) {
|
if (n.isFirstSibling() && n.getParent().UUID !== ROOT_NODE) {
|
||||||
await _notes2.current.goToNode(n.getParent()?.UUID, true, true)
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getParent()?.UUID, dontPush: true, dontExpand: true })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -161,14 +183,14 @@ export class TreeNative {
|
||||||
const siblingExpanded = this.getNodeExpanded(siblingBefore?.UUID)
|
const siblingExpanded = this.getNodeExpanded(siblingBefore?.UUID)
|
||||||
if (siblingBefore !== null && siblingExpanded && siblingBefore.hasChildren()) {
|
if (siblingBefore !== null && siblingExpanded && siblingBefore.hasChildren()) {
|
||||||
const siblingAbove = this.getLastExpandedNode(siblingBefore)
|
const siblingAbove = this.getLastExpandedNode(siblingBefore)
|
||||||
await _notes2.current.goToNode(siblingAbove?.UUID, true, true)
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: siblingAbove?.UUID, dontPush: true, dontExpand: true })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await _notes2.current.goToNode(n.getSiblingBefore()?.UUID, true, true)
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getSiblingBefore()?.UUID, dontPush: true, dontExpand: true })
|
||||||
}//}}}
|
}//}}}
|
||||||
async navigateRight(n) {//{{{
|
async navigateRight(n) {//{{{
|
||||||
if (n === null)
|
if (n === null || n === undefined)
|
||||||
return
|
return
|
||||||
|
|
||||||
const siblingAfter = n.getSiblingAfter()
|
const siblingAfter = n.getSiblingAfter()
|
||||||
|
|
@ -180,20 +202,20 @@ export class TreeNative {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expanded && n.hasChildren()) {
|
if (expanded && n.hasChildren()) {
|
||||||
await _notes2.current.goToNode(n.Children[0]?.UUID, true, true)
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: n.Children[0]?.UUID, dontPush: true, dontExpand: true })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (n.isLastSibling()) {
|
if (n.isLastSibling()) {
|
||||||
const nextNode = this.getParentWithNextSibling(n)
|
const nextNode = this.getParentWithNextSibling(n)
|
||||||
await _notes2.current.goToNode(nextNode?.UUID, true, true)
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: nextNode?.UUID, dontPush: true, dontExpand: true })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await _notes2.current.goToNode(n.getSiblingAfter()?.UUID, true, true)
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getSiblingAfter()?.UUID, dontPush: true, dontExpand: true })
|
||||||
}//}}}
|
}//}}}
|
||||||
async navigateUp(n) {//{{{
|
async navigateUp(n) {//{{{
|
||||||
if (n === null)
|
if (n === null || n === undefined)
|
||||||
return
|
return
|
||||||
|
|
||||||
let parent = null
|
let parent = null
|
||||||
|
|
@ -206,22 +228,22 @@ export class TreeNative {
|
||||||
parent = n.getParent()
|
parent = n.getParent()
|
||||||
if (parent?.UUID === ROOT_NODE)
|
if (parent?.UUID === ROOT_NODE)
|
||||||
return
|
return
|
||||||
await _notes2.current.goToNode(parent?.UUID, true, true)
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: parent?.UUID, dontPush: true, dontExpand: true })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (siblingBefore !== null && siblingExpanded && siblingBefore.hasChildren()) {
|
if (siblingBefore !== null && siblingExpanded && siblingBefore.hasChildren()) {
|
||||||
await _notes2.current.goToNode(siblingBefore.Children[siblingBefore.Children.length - 1]?.UUID, true, true)
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: siblingBefore.Children[siblingBefore.Children.length - 1]?.UUID, dontPush: true, dontExpand: true })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (siblingBefore) {
|
if (siblingBefore) {
|
||||||
await _notes2.current.goToNode(siblingBefore.UUID, true, true)
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: siblingBefore.UUID, dontPush: true, dontExpand: true })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}//}}}
|
}//}}}
|
||||||
async navigateDown(n) {//{{{
|
async navigateDown(n) {//{{{
|
||||||
if (n === null)
|
if (n === null || n === undefined)
|
||||||
return
|
return
|
||||||
|
|
||||||
const nodeExpanded = this.getNodeExpanded(n.UUID)
|
const nodeExpanded = this.getNodeExpanded(n.UUID)
|
||||||
|
|
@ -230,27 +252,27 @@ export class TreeNative {
|
||||||
// Traverse upward to nearest parent with next sibling.
|
// Traverse upward to nearest parent with next sibling.
|
||||||
if (!nodeExpanded && n.isLastSibling()) {
|
if (!nodeExpanded && n.isLastSibling()) {
|
||||||
const wantedNode = this.getParentWithNextSibling(n)
|
const wantedNode = this.getParentWithNextSibling(n)
|
||||||
await _notes2.current.goToNode(wantedNode?.UUID, true, true)
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: wantedNode?.UUID, dontPush: true, dontExpand: true })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nodeExpanded && n.isLastSibling() && !n.hasChildren()) {
|
if (nodeExpanded && n.isLastSibling() && !n.hasChildren()) {
|
||||||
const wantedNode = this.getParentWithNextSibling(n)
|
const wantedNode = this.getParentWithNextSibling(n)
|
||||||
await _notes2.current.goToNode(wantedNode?.UUID, true, true)
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: wantedNode?.UUID, dontPush: true, dontExpand: true })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Node not expanded. Go to this node's next sibling.
|
// Node not expanded. Go to this node's next sibling.
|
||||||
// GoToNode will abort if given null.
|
// GoToNode will abort if given null.
|
||||||
if (!nodeExpanded || !n.hasChildren()) {
|
if (!nodeExpanded || !n.hasChildren()) {
|
||||||
await _notes2.current.goToNode(n.getSiblingAfter()?.UUID, true, true)
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: n.getSiblingAfter()?.UUID, dontPush: true, dontExpand: true })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Node is expanded.
|
// Node is expanded.
|
||||||
// Children will be visually beneath this node, if any.
|
// Children will be visually beneath this node, if any.
|
||||||
if (nodeExpanded && n.hasChildren()) {
|
if (nodeExpanded && n.hasChildren()) {
|
||||||
await _notes2.current.goToNode(n.Children[0].UUID, true, true)
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: n.Children[0].UUID, dontPush: true, dontExpand: true })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}//}}}
|
}//}}}
|
||||||
|
|
@ -258,7 +280,7 @@ export class TreeNative {
|
||||||
const root = await nodeStore.get(ROOT_NODE)
|
const root = await nodeStore.get(ROOT_NODE)
|
||||||
if (root.Children.length === 0)
|
if (root.Children.length === 0)
|
||||||
return
|
return
|
||||||
await _notes2.current.goToNode(root.Children[0]?.UUID, true, true)
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: root.Children[0]?.UUID, dontPush: true, dontExpand: true })
|
||||||
}//}}}
|
}//}}}
|
||||||
async navigateBottom() {//{{{
|
async navigateBottom() {//{{{
|
||||||
const root = await nodeStore.get(ROOT_NODE)
|
const root = await nodeStore.get(ROOT_NODE)
|
||||||
|
|
@ -270,9 +292,9 @@ export class TreeNative {
|
||||||
|
|
||||||
if (toplevelExpanded) {
|
if (toplevelExpanded) {
|
||||||
const lastnode = this.getLastExpandedNode(toplevel)
|
const lastnode = this.getLastExpandedNode(toplevel)
|
||||||
await _notes2.current.goToNode(lastnode?.UUID, true, true)
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: lastnode?.UUID, dontPush: true, dontExpand: true })
|
||||||
} else
|
} else
|
||||||
await _notes2.current.goToNode(root.Children[root.Children.length - 1]?.UUID, true, true)
|
_mbus.dispatch("GO_TO_NODE", { nodeUUID: root.Children[root.Children.length - 1]?.UUID, dontPush: true, dontExpand: true })
|
||||||
}//}}}
|
}//}}}
|
||||||
|
|
||||||
getParentWithNextSibling(node) {//{{{
|
getParentWithNextSibling(node) {//{{{
|
||||||
|
|
@ -299,6 +321,17 @@ export class TreeNative {
|
||||||
if (!state)
|
if (!state)
|
||||||
await this.setNodeExpanded(node, false)
|
await this.setNodeExpanded(node, false)
|
||||||
}//}}}
|
}//}}}
|
||||||
|
async makeVisible(node) {// {{{
|
||||||
|
const treenode = this.treeNodeComponents[node.UUID]
|
||||||
|
|
||||||
|
const ancestors = await nodeStore.getNodeAncestry(node)
|
||||||
|
for (const ancestor of ancestors.reverse()) {
|
||||||
|
this.setNodeExpanded(ancestor, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The ROOT_NODE for example hasn't got a treenode.
|
||||||
|
treenode?.element.scrollIntoView({ block: 'nearest' })
|
||||||
|
}// }}}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TreeNodeNative {
|
export class TreeNodeNative {
|
||||||
|
|
@ -312,6 +345,7 @@ export class TreeNodeNative {
|
||||||
this.icon_expand = document.createElement('img')
|
this.icon_expand = document.createElement('img')
|
||||||
|
|
||||||
this.children_populated = false
|
this.children_populated = false
|
||||||
|
this.rendered = false
|
||||||
|
|
||||||
this.createElements()
|
this.createElements()
|
||||||
|
|
||||||
|
|
@ -323,7 +357,8 @@ export class TreeNodeNative {
|
||||||
this.render(true)
|
this.render(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.rendered = false
|
if (this.node.Level === 0 || this.tree.getNodeExpanded(this.node.UUID))
|
||||||
|
this.fetchChildren()
|
||||||
}//}}}
|
}//}}}
|
||||||
createElements() {// {{{
|
createElements() {// {{{
|
||||||
this.element.innerHTML = `
|
this.element.innerHTML = `
|
||||||
|
|
@ -334,7 +369,12 @@ export class TreeNodeNative {
|
||||||
|
|
||||||
this.element.children[0].addEventListener('click', ()=>this.tree.setNodeExpanded(this.node, !this.tree.getNodeExpanded(this.node.UUID)))
|
this.element.children[0].addEventListener('click', ()=>this.tree.setNodeExpanded(this.node, !this.tree.getNodeExpanded(this.node.UUID)))
|
||||||
this.element.children[0].appendChild(this.icon_expand)
|
this.element.children[0].appendChild(this.icon_expand)
|
||||||
this.element.children[1].addEventListener('click', ()=>window._notes2.current.goToNode(this.node.UUID))
|
|
||||||
|
this.element.children[1].addEventListener('click', ()=>_mbus.dispatch('TREE_NODE_SELECTED', this.node))
|
||||||
|
}// }}}
|
||||||
|
async fetchChildren() {//{{{
|
||||||
|
await this.node.fetchChildren()
|
||||||
|
this.children_populated = true
|
||||||
}//}}}
|
}//}}}
|
||||||
render(force_update) {//{{{
|
render(force_update) {//{{{
|
||||||
if (this.rendered && force_update !== true)
|
if (this.rendered && force_update !== true)
|
||||||
|
|
|
||||||
|
|
@ -46,16 +46,10 @@ html {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
#tree-native {
|
|
||||||
grid-area: tree;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
#tree {
|
#tree {
|
||||||
grid-area: tree;
|
grid-area: tree;
|
||||||
padding: 16px 32px;
|
display: grid;
|
||||||
background-color: #333;
|
padding: 16px 0px 16px 16px;
|
||||||
color: #ddd;
|
color: #ddd;
|
||||||
z-index: 100; // Over crumbs shadow
|
z-index: 100; // Over crumbs shadow
|
||||||
border-left: 2px solid #333;
|
border-left: 2px solid #333;
|
||||||
|
|
@ -68,9 +62,12 @@ html {
|
||||||
display: grid;
|
display: grid;
|
||||||
position: relative;
|
position: relative;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
|
margin-top: 8px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
margin-left: 24px;
|
margin-left: 24px;
|
||||||
margin-right: 24px;
|
margin-right: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 128px;
|
width: 128px;
|
||||||
left: -20px;
|
left: -20px;
|
||||||
|
|
@ -130,12 +127,19 @@ html {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#tree-nodes {
|
||||||
|
padding: 16px 32px;
|
||||||
|
background-color: #333;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 5px 5px 10px -5px rgba(0,0,0,0.75);
|
||||||
|
}
|
||||||
|
|
||||||
#crumbs {
|
#crumbs {
|
||||||
grid-area: crumbs;
|
grid-area: crumbs;
|
||||||
display: grid;
|
display: grid;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
margin: 0px 16px;
|
margin: 16px 16px;
|
||||||
|
|
||||||
.crumbs {
|
.crumbs {
|
||||||
background: #e4e4e4;
|
background: #e4e4e4;
|
||||||
|
|
@ -160,6 +164,11 @@ html {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.crumb:after {
|
.crumb:after {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,18 @@
|
||||||
{{ define "page" }}
|
{{ define "page" }}
|
||||||
<div id="tree"></div>
|
<div id="notes2">
|
||||||
<div id="crumbs">
|
<div id="tree" tabindex=0></div>
|
||||||
<div class="crumbs">
|
<div id="crumbs"></div>
|
||||||
</div>
|
<div id="sync-progress">
|
||||||
|
<!--
|
||||||
|
<progress min=0 max=1 value=0></progress>
|
||||||
|
<div class="count">0 / 1</div>
|
||||||
|
-->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="note"></div>
|
||||||
|
<!--div id="blank"></div-->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="notes2"></div>
|
|
||||||
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/notes2.css">
|
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/notes2.css">
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
|
|
@ -16,7 +23,7 @@ import 'preact/debug'
|
||||||
import 'preact/devtools'
|
import 'preact/devtools'
|
||||||
{{- end }}
|
{{- end }}
|
||||||
import { NodeStore } from 'node_store'
|
import { NodeStore } from 'node_store'
|
||||||
import { Notes2 } from "/js/{{ .VERSION }}/notes2.mjs"
|
import { App } from "/js/{{ .VERSION }}/app.mjs"
|
||||||
import { API } from 'api'
|
import { API } from 'api'
|
||||||
import { Sync } from 'sync'
|
import { Sync } from 'sync'
|
||||||
|
|
||||||
|
|
@ -29,8 +36,7 @@ if (!API.hasAuthenticationToken()) {
|
||||||
try {
|
try {
|
||||||
window.nodeStore = new NodeStore()
|
window.nodeStore = new NodeStore()
|
||||||
window.nodeStore.initializeDB().then(() => {
|
window.nodeStore.initializeDB().then(() => {
|
||||||
window._notes2 = createRef()
|
window._app = new App()
|
||||||
render(html`<${Notes2} ref=${window._notes2} />`, document.getElementById('notes2'))
|
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(e)
|
alert(e)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue