Upload files to IndexedDB
This commit is contained in:
parent
5bd5ef1f02
commit
8b421ea59e
15 changed files with 539 additions and 99 deletions
305
static/js/page_node.mjs
Normal file
305
static/js/page_node.mjs
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
import { ROOT_NODE, uuidv7, StoreFile } from 'node_store'
|
||||
import { CustomHTMLElement } from './lib/custom_html_element.mjs'
|
||||
import { MarkedPosition } from './marked_position.mjs'
|
||||
|
||||
export class N2PageNodeUI extends CustomHTMLElement {
|
||||
static {// {{{
|
||||
this.tmpl = document.createElement('template')
|
||||
this.tmpl.innerHTML = `
|
||||
<style>
|
||||
.el-functions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr min-content;
|
||||
grid-gap: 8px;
|
||||
align-items: center;
|
||||
justify-items: end;
|
||||
|
||||
img {
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<div data-el="name"></div>
|
||||
|
||||
<textarea data-el="node-content" required rows=1></textarea>
|
||||
<div data-el="node-markdown" tabindex=1></div>
|
||||
|
||||
<div data-el="functions">
|
||||
<img data-el="icon-markdown">
|
||||
<img data-el="icon-save" src="/images/${_VERSION}/icon_save_disabled.svg">
|
||||
</div>
|
||||
`
|
||||
}// }}}
|
||||
|
||||
constructor() {// {{{
|
||||
super()
|
||||
this.node = null
|
||||
|
||||
this.style.display = 'contents'
|
||||
this.classList.add('show-markdown') // TODO Should probably be moved to settings.
|
||||
this.marked = new MarkedPosition()
|
||||
|
||||
_mbus.subscribe('NODE_UI_OPEN', event => {
|
||||
this.node = event.detail.data
|
||||
this.showMarkdown(true)
|
||||
this.render()
|
||||
})
|
||||
|
||||
_mbus.subscribe('NODE_MODIFIED', () => {
|
||||
document.querySelector('#crumbs .crumbs')?.classList.add('node-modified')
|
||||
this.elIconSave.src = `/images/${_VERSION}/icon_save.svg`
|
||||
this.renderName()
|
||||
})
|
||||
|
||||
_mbus.subscribe('NODE_UNMODIFIED', () => {
|
||||
document.querySelector('#crumbs .crumbs')?.classList.remove('node-modified')
|
||||
this.elIconSave.src = `/images/${_VERSION}/icon_save_disabled.svg`
|
||||
})
|
||||
|
||||
_mbus.subscribe('MARKDOWN_TOGGLE', () => this.showMarkdown(!this.showMarkdown()))
|
||||
_mbus.subscribe('MARKDOWN_EDIT', ({ detail }) => this.editMarkdown(detail.data))
|
||||
|
||||
this.elName.addEventListener('click', () => {
|
||||
const name = prompt('Change title', this.node.data.Name)
|
||||
if (name === null)
|
||||
return
|
||||
|
||||
try {
|
||||
this.node.setName(name)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
alert(err)
|
||||
}
|
||||
})
|
||||
this.elNodeContent.addEventListener('input', event => this.contentChanged(event))
|
||||
this.elNodeContent.addEventListener('paste', async (event) => this.pasteHandler(event))
|
||||
this.elIconMarkdown.addEventListener('click', () => this.showMarkdown(!this.showMarkdown()))
|
||||
|
||||
this.showMarkdown(true)
|
||||
}// }}}
|
||||
renderName() {// {{{
|
||||
this.elName.innerText = this.node?.get('Name') ?? ''
|
||||
}// }}}
|
||||
render() {// {{{
|
||||
this.elName.innerText = this.node?.get('Name') ?? ''
|
||||
this.elNodeContent.value = this.node?.get('Content') ?? ''
|
||||
this.elNodeMarkdown.innerHTML = this.marked.parse(this.elNodeContent.value)
|
||||
}// }}}
|
||||
takeFocus() {// {{{
|
||||
if (this.showMarkdown()) {
|
||||
this.elNodeMarkdown.focus()
|
||||
} else
|
||||
this.elNodeContent.focus()
|
||||
}// }}}
|
||||
|
||||
contentChanged(event) {//{{{
|
||||
this.node.setContent(event.target.value)
|
||||
}//}}}
|
||||
isModified() {// {{{
|
||||
return this.node?.isModified()
|
||||
}// }}}
|
||||
showMarkdown(state) {// {{{
|
||||
// No point in showing markdown if there is no data.
|
||||
// If there is no data, it will show a blank page regardless, and the user will most
|
||||
// likely want to edit content, which can't be done in markdown.
|
||||
const show = this.node?.content().trim() !== '' && state
|
||||
|
||||
switch (show) {
|
||||
case true:
|
||||
this.elNodeMarkdown.innerHTML = this.marked.parse(this.elNodeContent.value)
|
||||
this.elIconMarkdown.src = `/images/${_VERSION}/icon_markdown.svg`
|
||||
this.classList.add('show-markdown')
|
||||
break
|
||||
case false:
|
||||
this.elIconMarkdown.src = `/images/${_VERSION}/icon_markdown_hollow.svg`
|
||||
this.classList.remove('show-markdown')
|
||||
break
|
||||
case null:
|
||||
case undefined:
|
||||
return this.classList.contains('show-markdown')
|
||||
}
|
||||
}// }}}
|
||||
async pasteHandler(event) {
|
||||
const clipboardItems = event.clipboardData?.items
|
||||
if (!clipboardItems)
|
||||
return
|
||||
|
||||
for (const item of clipboardItems) {
|
||||
switch (item.kind) {
|
||||
case 'string':
|
||||
continue
|
||||
|
||||
case 'file':
|
||||
const file = item.getAsFile()
|
||||
if (!file)
|
||||
throw new Error("Couldn't convert image to file object.")
|
||||
const uuid = uuidv7()
|
||||
await globalThis.nodeStore.files.add({ data: { UUID: uuid, file: file }})
|
||||
|
||||
const [start, end] = [this.elNodeContent.selectionStart, this.elNodeContent.selectionEnd]
|
||||
this.elNodeContent.setRangeText(``, start, end, 'select');
|
||||
|
||||
break
|
||||
|
||||
default:
|
||||
alert(`Unknown paste type of '${item.kind}'`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example usage: Displaying the image or preparing it for upload
|
||||
handleImageBlob(blob) {
|
||||
// 1. Create a local URL to preview it in an <img> tag if needed
|
||||
const localUrl = URL.createObjectURL(blob)
|
||||
console.log('Local preview URL:', localUrl)
|
||||
|
||||
// 2. Or prepare it for a FormData upload
|
||||
const formData = new FormData()
|
||||
formData.append('image', blob, 'pasted-image.png')
|
||||
|
||||
// fetch('/upload', { method: 'POST', body: formData })
|
||||
}
|
||||
|
||||
editMarkdown(data) {// {{{
|
||||
this.showMarkdown(false)
|
||||
this.elNodeContent.selectionStart = data.position.start
|
||||
this.elNodeContent.selectionEnd = data.position.end
|
||||
this.elNodeContent.focus()
|
||||
}// }}}
|
||||
}
|
||||
customElements.define('n2-nodeui', N2PageNodeUI)
|
||||
|
||||
export class Node {
|
||||
static sort(a, b) {//{{{
|
||||
if (a.data.Name < b.data.Name) return -1
|
||||
if (a.data.Name > b.data.Name) return 0
|
||||
return 0
|
||||
}//}}}
|
||||
static create(name, parentUUID) {// {{{
|
||||
return new Node({
|
||||
UUID: uuidv7(),
|
||||
Created: (new Date()).toISOString(),
|
||||
Content: '',
|
||||
Name: name,
|
||||
ParentUUID: parentUUID,
|
||||
Markdown: false,
|
||||
History: false,
|
||||
})
|
||||
}// }}}
|
||||
|
||||
constructor(nodeData, level) {//{{{
|
||||
this.Level = level
|
||||
this.data = nodeData
|
||||
this.UUID = nodeData.UUID
|
||||
|
||||
// Toplevel nodes are normalized to have the ROOT_NODE as parent.
|
||||
if (nodeData.UUID !== ROOT_NODE && nodeData.ParentUUID === '') {
|
||||
this.ParentUUID = ROOT_NODE
|
||||
this.data.ParentUUID = ROOT_NODE
|
||||
} else
|
||||
this.ParentUUID = nodeData.ParentUUID
|
||||
|
||||
this._children_fetched = false
|
||||
this.Children = []
|
||||
this.Ancestors = []
|
||||
|
||||
this._sibling_before = null
|
||||
this._sibling_after = null
|
||||
this._parent = null
|
||||
|
||||
this.reset()
|
||||
}//}}}
|
||||
|
||||
reset() {// {{{
|
||||
this._content = this.data.Content
|
||||
this._modified = false
|
||||
}// }}}
|
||||
get(prop) {//{{{
|
||||
return this.data[prop]
|
||||
}//}}}
|
||||
updated() {//{{{
|
||||
// '2024-12-17T17:33:48.85939Z
|
||||
return new Date(Date.parse(this.data.Updated))
|
||||
}//}}}
|
||||
isModified() {// {{{
|
||||
return this._modified
|
||||
}// }}}
|
||||
hasFetchedChildren() {//{{{
|
||||
return this._children_fetched
|
||||
}//}}}
|
||||
async fetchChildren() {//{{{
|
||||
this.Children = await nodeStore.getTreeNodes(this.UUID, this.Level + 1)
|
||||
this._children_fetched = true
|
||||
|
||||
// Children are sorted to allow for storing siblings befare and after.
|
||||
// These are used with keyboard navigation in the tree.
|
||||
this.Children.sort(Node.sort)
|
||||
|
||||
const numChildren = this.Children.length
|
||||
for (let i = 0; i < numChildren; i++) {
|
||||
if (i > 0)
|
||||
this.Children[i]._sibling_before = this.Children[i - 1]
|
||||
if (i < numChildren - 1)
|
||||
this.Children[i]._sibling_after = this.Children[i + 1]
|
||||
this.Children[i]._parent = this
|
||||
}
|
||||
|
||||
// Notify the tree that all children are fetched and ready to process.
|
||||
//_notes2.current.tree.fetchChildrenOn(this.UUID)
|
||||
_mbus.dispatch(`NODE_CHILDREN_FETCHED_${this.UUID}`)
|
||||
|
||||
return this.Children
|
||||
}//}}}
|
||||
hasChildren() {//{{{
|
||||
return this.Children.length > 0
|
||||
}//}}}
|
||||
getSiblingBefore() {// {{{
|
||||
return this._sibling_before
|
||||
}// }}}
|
||||
getSiblingAfter() {// {{{
|
||||
return this._sibling_after
|
||||
}// }}}
|
||||
getParent() {//{{{
|
||||
return this._parent
|
||||
}//}}}
|
||||
isLastSibling() {//{{{
|
||||
return this._sibling_after === null
|
||||
}//}}}
|
||||
isFirstSibling() {//{{{
|
||||
return this._sibling_before === null
|
||||
}//}}}
|
||||
content() {//{{{
|
||||
/* TODO - implement crypto
|
||||
if (this.CryptoKeyID != 0 && !this._decrypted)
|
||||
this.#decrypt()
|
||||
*/
|
||||
return this._content
|
||||
}//}}}
|
||||
setContent(new_content) {//{{{
|
||||
this._content = new_content
|
||||
this._modified = true
|
||||
_mbus.dispatch('NODE_MODIFIED', { node: this })
|
||||
}//}}}
|
||||
setName(new_name) {// {{{
|
||||
if (new_name.trim() === '')
|
||||
throw new Error(`The name can't be empty`)
|
||||
|
||||
this.data.Name = new_name
|
||||
this._modified = true
|
||||
_mbus.dispatch('NODE_MODIFIED', { node: this })
|
||||
}// }}}
|
||||
async save() {//{{{
|
||||
this.data.Content = this._content
|
||||
this.data.Updated = new Date().toISOString()
|
||||
this._modified = false
|
||||
|
||||
_mbus.dispatch('NODE_UNMODIFIED')
|
||||
|
||||
// When stored into database and ancestry was changed,
|
||||
// the ancestry path could be interesting.
|
||||
const ancestors = await nodeStore.getNodeAncestry(this)
|
||||
this.data.Ancestors = ancestors.map(a => a.get('Name')).reverse()
|
||||
}//}}}
|
||||
}
|
||||
|
||||
// vim: foldmethod=marker
|
||||
Loading…
Add table
Add a link
Reference in a new issue