import { h, Component, createRef } from 'preact' import htm from 'htm' import { signal } from 'preact/signals' import { ROOT_NODE } from 'node_store' const html = htm.bind(h) export class NodeUI extends Component { constructor(props) {//{{{ super(props) this.menu = signal(false) this.node = signal(null) this.nodeContent = createRef() this.nodeProperties = createRef() this.nodeModified = signal(false) this.keys = signal([]) this.page = signal('node') window.addEventListener('popstate', evt => { if (evt.state?.hasOwnProperty('nodeUUID')) this.goToNode(evt.state.nodeUUID, true) else this.goToNode(0, true) }) window.addEventListener('keydown', evt => this.keyHandler(evt)) }//}}} render() {//{{{ if (this.node.value === null) return const node = this.node.value document.title = node.Name return html`
_notes2.current.goToNode(ROOT_NODE)}>Start
Minnie
Fluffy
Chili
${node.Name}
<${NodeContent} key=${node.UUID} node=${node} ref=${this.nodeContent} />
` return let crumbs = [ html`
this.goToNode(0)}>Start
` ] crumbs = crumbs.concat(node.Crumbs.slice(0).map(node => html`
this.goToNode(node.ID)}>${node.Name}
` ).reverse()) let modified = '' if (this.props.app.nodeModified.value) modified = 'modified' // Page to display let page = '' switch (this.page.value) { case 'node': if (node.ID === 0) { page = html`
{ this.page.value = 'schedule-events' }}>Schedule events
${children.length > 0 ? html`
${children}
Notes version ${window._VERSION}
` : html``} ` } else { let padlock = '' if (node.CryptoKeyID > 0) padlock = html`` page = html` ${children.length > 0 ? html`
${children}
` : html``}
${node.Name} ${padlock}
<${NodeContent} key=${node.ID} node=${node} ref=${this.nodeContent} /> <${NodeEvents} events=${node.ScheduleEvents.value} /> <${Checklist} ui=${this} groups=${node.ChecklistGroups} /> <${NodeFiles} node=${this.node.value} /> ` } break case 'upload': page = html`<${UploadUI} nodeui=${this} />` break case 'node-properties': page = html`<${NodeProperties} ref=${this.nodeProperties} nodeui=${this} />` break case 'keys': page = html`<${Keys} nodeui=${this} />` break case 'profile-settings': page = html`<${ProfileSettings} nodeui=${this} />` break case 'search': page = html`<${Search} nodeui=${this} />` break case 'schedule-events': page = html`<${ScheduleEventList} nodeui=${this} />` break } const menu = () => (this.menu.value ? html`<${Menu} nodeui=${this} />` : null) const checklist = () => html`
{ evt.stopPropagation(); this.toggleChecklist() }}>
` return html` <${menu} />
${crumbs}
${page} ` }//}}} async componentDidMount() {//{{{ _notes2.current.goToNode(this.props.startNode.UUID, true) }//}}} setNode(node) {//{{{ this.nodeModified.value = false this.node.value = node }//}}} keyHandler(evt) {//{{{ return 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 (!(evt.shiftKey && evt.altKey) && !(evt.key.toUpperCase() == 'S' && evt.ctrlKey)) return switch (evt.key.toUpperCase()) { 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': if (this.page.value == 'node') 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) { evt.preventDefault() evt.stopPropagation() } }//}}} } class NodeContent extends Component { constructor(props) {//{{{ super(props) this.contentDiv = createRef() this.state = { modified: false, } }//}}} render({ node }) {//{{{ let content = '' try { content = node.content() } catch (err) { return html`
${err.message}
` } let element /* if (node.RenderMarkdown.value) element = html`<${MarkdownContent} key='markdown-content' content=${content} />` else */ element = html`
` return element }//}}} componentDidMount() {//{{{ this.resize() window.addEventListener('resize', () => this.resize()) const contentResizeObserver = new ResizeObserver(entries => { for (const idx in entries) { const w = entries[idx].contentRect.width document.querySelector('#crumbs .crumbs').style.width = `${w}px` } }); const nodeContent = document.getElementById('node-content') contentResizeObserver.observe(nodeContent); }//}}} componentDidUpdate() {//{{{ this.resize() }//}}} contentChanged(evt) {//{{{ _notes2.current.nodeUI.current.nodeModified.value = true const content = evt.target.value this.props.node.setContent(content) this.resize() }//}}} resize() {//{{{ const textarea = document.getElementById('node-content') if (textarea) textarea.parentNode.dataset.replicatedValue = textarea.value }//}}} } export class Node { constructor(nodeData, level) {//{{{ this.Level = level this.data = nodeData this.UUID = nodeData.UUID this.ParentUUID = nodeData.ParentUUID this._children_fetched = false this.Children = [] /* this.RenderMarkdown = signal(nodeData.RenderMarkdown) this.Markdown = false this.ShowChecklist = signal(false) this._content = nodeData.Content this.Crumbs = [] this.Files = [] this._decrypted = false this._expanded = false // start value for the TreeNode component, this.ChecklistGroups = {} this.ScheduleEvents = signal([]) // it doesn't control it afterwards. // Used to expand the crumbs upon site loading. */ }//}}} get(prop) {//{{{ return this.data[prop] }//}}} updated() {//{{{ // '2024-12-17T17:33:48.85939Z return new Date(Date.parse(this.data.Updated)) }//}}} hasFetchedChildren() {//{{{ return this._children_fetched }//}}} async fetchChildren() {//{{{ if (this._children_fetched) return this.Children this.Children = await nodeStore.getTreeNodes(this.UUID, this.Level + 1) this._children_fetched = true return this.Children }//}}} content() {//{{{ /* TODO - implement crypto if (this.CryptoKeyID != 0 && !this._decrypted) this.#decrypt() */ return this.data.Content }//}}} setContent(new_content) {//{{{ this._content = new_content /* TODO - implement crypto if (this.CryptoKeyID == 0) // Logic behind plaintext not being decrypted is that // only encrypted values can be in a decrypted state. this._decrypted = false else this._decrypted = true */ }//}}} static sort(a, b) {//{{{ if (a.data.Name < b.data.Name) return -1 if (a.data.Name > b.data.Name) return 0 return 0 }//}}} } // vim: foldmethod=marker