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') this.crumbs = [] window.addEventListener('popstate', evt => { if (evt.state?.hasOwnProperty('nodeUUID')) _notes2.current.goToNode(evt.state.nodeUUID, true) else _notes2.current.goToNode('00000000-0000-0000-0000-000000000000', true) }) window.addEventListener('keydown', evt => this.keyHandler(evt)) }//}}} render() {//{{{ if (this.node.value === null) return const node = this.node.value document.title = node.get('Name') const nodeModified = this.nodeModified.value ? 'node-modified' : '' const crumbDivs = [ html`
_notes2.current.goToNode(ROOT_NODE)}>Start
` ] for (let i = this.crumbs.length-1; i >= 0; i--) { const crumbNode = this.crumbs[i] crumbDivs.push(html`
_notes2.current.goToNode(crumbNode.UUID)}>${crumbNode.get('Name')}
`) } if (node.UUID !== ROOT_NODE) crumbDivs.push( html`
_notes2.current.goToNode(node.UUID)}>${node.get('Name')}
` ) return html`
this.saveNode()}>
${crumbDivs}
${node.get('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()) // 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) _notes2.current.tree.expandToTrunk(this.props.startNode) }//}}} setNode(node) {//{{{ this.nodeModified.value = false this.node.value = node }//}}} setCrumbs(nodes) {//{{{ this.crumbs = nodes }//}}} async saveNode() {//{{{ if (!this.nodeModified.value) return await nodeStore.copyToNodesHistory(this.node.value) // Prepares the node object for saving. // Sets Updated value to current date and time. const node = this.node.value node.save() await nodeStore.add([node]) this.nodeModified.value = false }//}}} keyHandler(evt) {//{{{ 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 */ const 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 this.props.node.setContent(evt.target.value) this.resize() }//}}} resize() {//{{{ const textarea = document.getElementById('node-content') if (textarea) textarea.parentNode.dataset.replicatedValue = textarea.value }//}}} } 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 }//}}} constructor(nodeData, level) {//{{{ this.Level = level this.data = nodeData this.UUID = nodeData.UUID this.ParentUUID = nodeData.ParentUUID this._children_fetched = false this.Children = [] this._content = this.data.Content this._modified = false /* 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() */ this.modified = true return this._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 */ }//}}} save() {//{{{ this.data.Content = this._content this.data.Updated = new Date().toISOString() this._modified = false }//}}} } // vim: foldmethod=marker