diff --git a/main.go b/main.go index 8edd01c..d9e693b 100644 --- a/main.go +++ b/main.go @@ -23,7 +23,7 @@ import ( "text/template" ) -const VERSION = "v5" +const VERSION = "v4" const CONTEXT_USER = 1 const SYNC_PAGINATION = 200 diff --git a/static/css/notes2.css b/static/css/notes2.css index 8351858..7c2320f 100644 --- a/static/css/notes2.css +++ b/static/css/notes2.css @@ -52,51 +52,51 @@ html { #tree { grid-area: tree; display: grid; - background-color: #ffffff; + background-color: #fafafa; color: #444; z-index: 100; - border-right: 2px solid #ddd; + border-right: 1px solid #ddd; + + n2-tree { + /*border: 2px solid #f8f8f8;*/ + padding: 16px 48px 16px 24px; + } + + &:focus-within { + n2-tree { + /* + border: 2px solid #fe5f55; + */ + } + + } + #logo { display: grid; - grid-template-columns: min-content 1fr min-content; - align-items: center; - justify-items: start; + position: relative; + justify-items: center; + margin-top: 8px; + margin-bottom: 8px; + margin-left: 24px; + margin-right: 24px; cursor: pointer; - padding: 16px; - border-bottom: 1px solid #ccc; - .el-search { - justify-self: end; - } + img { + width: 128px; + left: -20px; - img:first-child { - height: 24px; - margin-right: 8px; } } .icons { display: flex; justify-content: center; - margin: 16px 0px 32px 0px; + margin-bottom: 32px; gap: 8px; } - n2-tree { - .el-treenodes { - margin: 32px; - } - } - - &:focus-within { - n2-tree { - } - - } - - .node { display: grid; grid-template-columns: 40px min-content; @@ -145,6 +145,16 @@ html { } } +#tree-nodes { + padding: 16px 32px; + /* + border-radius: 8px; +*/ + /* + box-shadow: 5px 5px 10px -5px rgba(0, 0, 0, 0.75); + */ +} + #crumbs { grid-area: crumbs; display: grid; diff --git a/static/favicon.ico b/static/favicon.ico deleted file mode 100644 index 299310f..0000000 Binary files a/static/favicon.ico and /dev/null differ diff --git a/static/images/collapsed.svg b/static/images/collapsed.svg index db06415..d93f4ca 100644 --- a/static/images/collapsed.svg +++ b/static/images/collapsed.svg @@ -8,7 +8,7 @@ version="1.1" id="svg1" sodipodi:docname="collapsed.svg" - inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)" + inkscape:version="1.4.2 (ebf0e94, 2025-05-08)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" @@ -23,13 +23,13 @@ inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:document-units="px" - inkscape:zoom="19.349237" - inkscape:cx="11.809251" - inkscape:cy="6.3051583" - inkscape:window-width="1093" - inkscape:window-height="1401" - inkscape:window-x="2560" - inkscape:window-y="0" + inkscape:zoom="4.8373092" + inkscape:cx="6.201795" + inkscape:cy="-12.40359" + inkscape:window-width="1916" + inkscape:window-height="1161" + inkscape:window-x="0" + inkscape:window-y="18" inkscape:window-maximized="1" inkscape:current-layer="layer1" showguides="false" /> @@ -42,13 +42,9 @@ transform="translate(-102.39375,-146.31458)"> folder-outline - + style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#71c837;fill-opacity:1;stroke-width:0.330728;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1" /> diff --git a/static/images/expanded.svg b/static/images/expanded.svg index 9b420a8..017e8a4 100644 --- a/static/images/expanded.svg +++ b/static/images/expanded.svg @@ -8,7 +8,7 @@ version="1.1" id="svg1" sodipodi:docname="expanded.svg" - inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)" + inkscape:version="1.4.2 (ebf0e94, 2025-05-08)" xml:space="preserve" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" @@ -23,13 +23,13 @@ inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:document-units="px" - inkscape:zoom="15.807429" - inkscape:cx="10.533022" - inkscape:cy="16.384701" - inkscape:window-width="1093" - inkscape:window-height="1401" - inkscape:window-x="1463" - inkscape:window-y="0" + inkscape:zoom="11.17754" + inkscape:cx="20.845374" + inkscape:cy="26.929003" + inkscape:window-width="1916" + inkscape:window-height="1161" + inkscape:window-x="0" + inkscape:window-y="18" inkscape:window-maximized="1" inkscape:current-layer="layer1" />folder-openfolder-open-outline + style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#71c837;fill-opacity:1;stroke-width:0.264583;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1" /> diff --git a/static/images/icon_settings.svg b/static/images/icon_settings.svg deleted file mode 100644 index 1b692ad..0000000 --- a/static/images/icon_settings.svg +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - cog-outline - - - diff --git a/static/images/logo.svg b/static/images/logo.svg index f294234..3b5efa4 100644 --- a/static/images/logo.svg +++ b/static/images/logo.svg @@ -2,12 +2,12 @@ - - - NOTES - 2 - + transform="translate(-66.410416,-139.17084)"> + + + + diff --git a/static/images/logo_small.svg b/static/images/logo_small.svg deleted file mode 100644 index cb83d39..0000000 --- a/static/images/logo_small.svg +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - - - - N2 - - diff --git a/static/js/lib/css_colorize.mjs b/static/js/lib/css_colorize.mjs deleted file mode 100644 index f0fdb37..0000000 --- a/static/js/lib/css_colorize.mjs +++ /dev/null @@ -1,207 +0,0 @@ -export class Color { - constructor(r, g, b) { this.set(r, g, b); } - toString() { return `rgb(${Math.round(this.r)}, ${Math.round(this.g)}, ${Math.round(this.b)})`; } - - set(r, g, b) { - this.r = this.clamp(r); - this.g = this.clamp(g); - this.b = this.clamp(b); - } - - hueRotate(angle = 0) { - angle = angle / 180 * Math.PI; - let sin = Math.sin(angle); - let cos = Math.cos(angle); - - this.multiply([ - 0.213 + cos * 0.787 - sin * 0.213, 0.715 - cos * 0.715 - sin * 0.715, 0.072 - cos * 0.072 + sin * 0.928, - 0.213 - cos * 0.213 + sin * 0.143, 0.715 + cos * 0.285 + sin * 0.140, 0.072 - cos * 0.072 - sin * 0.283, - 0.213 - cos * 0.213 - sin * 0.787, 0.715 - cos * 0.715 + sin * 0.715, 0.072 + cos * 0.928 + sin * 0.072 - ]); - } - - grayscale(value = 1) { - this.multiply([ - 0.2126 + 0.7874 * (1 - value), 0.7152 - 0.7152 * (1 - value), 0.0722 - 0.0722 * (1 - value), - 0.2126 - 0.2126 * (1 - value), 0.7152 + 0.2848 * (1 - value), 0.0722 - 0.0722 * (1 - value), - 0.2126 - 0.2126 * (1 - value), 0.7152 - 0.7152 * (1 - value), 0.0722 + 0.9278 * (1 - value) - ]); - } - - sepia(value = 1) { - this.multiply([ - 0.393 + 0.607 * (1 - value), 0.769 - 0.769 * (1 - value), 0.189 - 0.189 * (1 - value), - 0.349 - 0.349 * (1 - value), 0.686 + 0.314 * (1 - value), 0.168 - 0.168 * (1 - value), - 0.272 - 0.272 * (1 - value), 0.534 - 0.534 * (1 - value), 0.131 + 0.869 * (1 - value) - ]); - } - - saturate(value = 1) { - this.multiply([ - 0.213 + 0.787 * value, 0.715 - 0.715 * value, 0.072 - 0.072 * value, - 0.213 - 0.213 * value, 0.715 + 0.285 * value, 0.072 - 0.072 * value, - 0.213 - 0.213 * value, 0.715 - 0.715 * value, 0.072 + 0.928 * value - ]); - } - - multiply(matrix) { - let newR = this.clamp(this.r * matrix[0] + this.g * matrix[1] + this.b * matrix[2]); - let newG = this.clamp(this.r * matrix[3] + this.g * matrix[4] + this.b * matrix[5]); - let newB = this.clamp(this.r * matrix[6] + this.g * matrix[7] + this.b * matrix[8]); - this.r = newR; this.g = newG; this.b = newB; - } - - brightness(value = 1) { this.linear(value); } - contrast(value = 1) { this.linear(value, -(0.5 * value) + 0.5); } - - linear(slope = 1, intercept = 0) { - this.r = this.clamp(this.r * slope + intercept * 255); - this.g = this.clamp(this.g * slope + intercept * 255); - this.b = this.clamp(this.b * slope + intercept * 255); - } - - invert(value = 1) { - this.r = this.clamp((value + (this.r / 255) * (1 - 2 * value)) * 255); - this.g = this.clamp((value + (this.g / 255) * (1 - 2 * value)) * 255); - this.b = this.clamp((value + (this.b / 255) * (1 - 2 * value)) * 255); - } - - hsl() { // Code taken from https://stackoverflow.com/a/9493060/2688027, licensed under CC BY-SA. - let r = this.r / 255; - let g = this.g / 255; - let b = this.b / 255; - let max = Math.max(r, g, b); - let min = Math.min(r, g, b); - let h, s, l = (max + min) / 2; - - if (max === min) { - h = s = 0; - } else { - let d = max - min; - s = l > 0.5 ? d / (2 - max - min) : d / (max + min); - switch (max) { - case r: h = (g - b) / d + (g < b ? 6 : 0); break; - case g: h = (b - r) / d + 2; break; - case b: h = (r - g) / d + 4; break; - } h /= 6; - } - - return { - h: h * 100, - s: s * 100, - l: l * 100 - }; - } - - clamp(value) { - if (value > 255) { value = 255; } - else if (value < 0) { value = 0; } - return value; - } -} - -export class Solver { - constructor(target) { - this.target = target; - this.targetHSL = target.hsl(); - this.reusedColor = new Color(0, 0, 0); // Object pool - } - - solve() { - let result = this.solveNarrow(this.solveWide()); - return { - values: result.values, - loss: result.loss, - filter: this.css(result.values) - }; - } - - solveWide() { - const A = 5; - const c = 15; - const a = [60, 180, 18000, 600, 1.2, 1.2]; - - let best = { loss: Infinity }; - for (let i = 0; best.loss > 25 && i < 3; i++) { - let initial = [50, 20, 3750, 50, 100, 100]; - let result = this.spsa(A, a, c, initial, 1000); - if (result.loss < best.loss) { best = result; } - } return best; - } - - solveNarrow(wide) { - const A = wide.loss; - const c = 2; - const A1 = A + 1; - const a = [0.25 * A1, 0.25 * A1, A1, 0.25 * A1, 0.2 * A1, 0.2 * A1]; - return this.spsa(A, a, c, wide.values, 500); - } - - spsa(A, a, c, values, iters) { - const alpha = 1; - const gamma = 0.16666666666666666; - - let best = null; - let bestLoss = Infinity; - let deltas = new Array(6); - let highArgs = new Array(6); - let lowArgs = new Array(6); - - for (let k = 0; k < iters; k++) { - let ck = c / Math.pow(k + 1, gamma); - for (let i = 0; i < 6; i++) { - deltas[i] = Math.random() > 0.5 ? 1 : -1; - highArgs[i] = values[i] + ck * deltas[i]; - lowArgs[i] = values[i] - ck * deltas[i]; - } - - let lossDiff = this.loss(highArgs) - this.loss(lowArgs); - for (let i = 0; i < 6; i++) { - let g = lossDiff / (2 * ck) * deltas[i]; - let ak = a[i] / Math.pow(A + k + 1, alpha); - values[i] = fix(values[i] - ak * g, i); - } - - let loss = this.loss(values); - if (loss < bestLoss) { best = values.slice(0); bestLoss = loss; } - } return { values: best, loss: bestLoss }; - - function fix(value, idx) { - let max = 100; - if (idx === 2 /* saturate */) { max = 7500; } - else if (idx === 4 /* brightness */ || idx === 5 /* contrast */) { max = 200; } - - if (idx === 3 /* hue-rotate */) { - if (value > max) { value = value % max; } - else if (value < 0) { value = max + value % max; } - } else if (value < 0) { value = 0; } - else if (value > max) { value = max; } - return value; - } - } - - loss(filters) { // Argument is array of percentages. - let color = this.reusedColor; - color.set(0, 0, 0); - - color.invert(filters[0] / 100); - color.sepia(filters[1] / 100); - color.saturate(filters[2] / 100); - color.hueRotate(filters[3] * 3.6); - color.brightness(filters[4] / 100); - color.contrast(filters[5] / 100); - - let colorHSL = color.hsl(); - return Math.abs(color.r - this.target.r) - + Math.abs(color.g - this.target.g) - + Math.abs(color.b - this.target.b) - + Math.abs(colorHSL.h - this.targetHSL.h) - + Math.abs(colorHSL.s - this.targetHSL.s) - + Math.abs(colorHSL.l - this.targetHSL.l); - } - - css(filters) { - function fmt(idx, multiplier = 1) { return Math.round(filters[idx] * multiplier); } - return `invert(${fmt(0)}%) sepia(${fmt(1)}%) saturate(${fmt(2)}%) hue-rotate(${fmt(3, 3.6)}deg) brightness(${fmt(4)}%) contrast(${fmt(5)}%)`; - } -} diff --git a/static/js/marked_position.mjs b/static/js/marked_position.mjs index 5c9c0ff..ca85d3e 100644 --- a/static/js/marked_position.mjs +++ b/static/js/marked_position.mjs @@ -110,12 +110,12 @@ export class MarkedPosition { renderer: { heading(token) { const content = this.parser.parseInline(token.tokens) - return `${content}\n` + return `${content}\n` }, paragraph(token) { const content = this.parser.parseInline(token.tokens) - return `

${content}

\n` + return `

${content}

\n` }, list(token) { @@ -134,7 +134,7 @@ export class MarkedPosition { }, listitem(token) { - return `
  • ${this.parser.parse(token.tokens)}
  • \n` + return `
  • ${this.parser.parse(token.tokens)}
  • \n` }, code(token) { @@ -143,12 +143,12 @@ export class MarkedPosition { const code = token.text.replace(other.endingNewline, '') + '\n' if (!langString) { - return `
    `
    +						return `
    `
     							+ (token.escaped ? code : escapeHtmlEntities(code, true))
     							+ '
    \n' } - return `
    '
     						+ (token.escaped ? code : escapeHtmlEntities(code, true))
    @@ -157,7 +157,7 @@ export class MarkedPosition {
     
     				blockquote(token) {
     					const body = this.parser.parse(token.tokens)
    -					return `
    \n${body}
    \n` + return `
    \n${body}
    \n` }, html(token) { @@ -169,11 +169,11 @@ export class MarkedPosition { }, hr(token) { - return `
    \n` + return `
    \n` }, checkbox(token) { - return ` ' }, @@ -218,7 +218,7 @@ export class MarkedPosition { if (token.tokens.length > 0) { const start = token.tokens[0].position.start.offset const end = token.tokens[0].position.end.offset - ofs = `ondblclick="setpos(event)" data-offset-start="${start}" data-offset-end="${end}"` + ofs = `onclick="setpos(event)" data-offset-start="${start}" data-offset-end="${end}"` } const content = this.parser.parseInline(token.tokens); @@ -230,23 +230,23 @@ export class MarkedPosition { }, strong(token) { - return `${this.parser.parseInline(token.tokens)}` + return `${this.parser.parseInline(token.tokens)}` }, em(token) { - return `${this.parser.parseInline(token.tokens)}` + return `${this.parser.parseInline(token.tokens)}` }, codespan(token) { - return `${escapeHtmlEntities(token.text, true)}` + return `${escapeHtmlEntities(token.text, true)}` }, br(token) { - return `
    ` + return `
    ` }, del(token) { - return `${this.parser.parseInline(token.tokens)}` + return `${this.parser.parseInline(token.tokens)}` }, link(token) { @@ -256,7 +256,7 @@ export class MarkedPosition { return text } token.href = cleanHref - let out = ' { + this.treeNodeComponents[node.UUID] = createRef() + return html`<${TreeNode} key=${`treenode_${node.UUID}`} tree=${this} node=${node} ref=${this.treeNodeComponents[node.UUID]} selected=${node.UUID === app.state.startNode?.UUID} />` + }) + + return html` +
    + +
    + _mbus.dispatch('op-search')} /> + _sync.run()} /> +
    + ${renderedTreeTrunk} +
    ` + }//}}} + componentDidMount() {//{{{ + //this.treeDiv.current.addEventListener('keydown', event => this.keyHandler(event)) + + // This will show and select the treenode that is selected in the node UI. + const node = _notes2.current?.nodeUI.current?.node.value + if (node === null) + return + _notes2.current.tree.expandToTrunk(node) + this.setSelected(node) + }//}}} + + fetchChildrenNotify(uuid, fn) {//{{{ + if (this.childrenFetchedCallbacks[uuid] === undefined) + this.childrenFetchedCallbacks[uuid] = [fn] + else + this.childrenFetchedCallbacks[uuid].push(fn) + }//}}} + fetchChildrenOn(uuid) {//{{{ + if (this.childrenFetchedCallbacks[uuid] === undefined) + return + for (const fn of this.childrenFetchedCallbacks[uuid]) + fn(uuid) + delete this.childrenFetchedCallbacks[uuid] + }//}}} + + populateFirstLevel(callback = null) {//{{{ + nodeStore.get(ROOT_NODE) + .then(node => node.fetchChildren()) + .then(children => { + this.treeNodeComponents = {} + this.treeTrunk = [] + for (const node of children) { + // The root node isn't supposed to be shown in the tree. + if (node.UUID === ROOT_NODE) + continue + if (node.ParentUUID === ROOT_NODE) + this.treeTrunk.push(node) + } + this.forceUpdate() + if (callback) + callback() + + }) + .catch(e => { console.log(e); console.log(e.type, e.error); alert(e.error) }) + }//}}} + setSelected(node, dontExpand) {//{{{ + // 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]?.current.forceUpdate() + + // And now the newly selected node is rerendered. + this.treeNodeComponents[node.UUID]?.current.forceUpdate() + + if (!dontExpand) + this.setNodeExpanded(node, true) + }//}}} + isSelected(node) {//{{{ + return this.selectedNode?.UUID === node.UUID + }//}}} + async expandToTrunk(node) {//{{{ + // Get all ancestors from a certain node up to the highest grandparent. + const ancestry = await nodeStore.getNodeAncestry(node, []) + for (const i in ancestry) { + await nodeStore.node(ancestry[i].UUID).fetchChildren() + this.setNodeExpanded(ancestry[i], true) + } + + // Already a top node, no need to expand anything. + if (ancestry.length === 0) + return + + // Start the chain of by expanding the top node. + this.setNodeExpanded(ancestry[ancestry.length - 1], true) + }//}}} + getNodeExpanded(UUID) {//{{{ + if (this.expandedNodes[UUID] === undefined) + this.expandedNodes[UUID] = signal(false) + return this.expandedNodes[UUID].value + }//}}} + async setNodeExpanded(node, value) {//{{{ + return new Promise((resolve, reject) => { + const work = uuid => { + // Creating a default value if it doesn't exist already. + this.getNodeExpanded(uuid) + this.expandedNodes[uuid].value = value + resolve() + } + + if (node.hasFetchedChildren()) { + work(node.UUID) + return + } else { + this.fetchChildrenNotify(node.UUID, uuid => work(uuid)) + } + }) + }//}}} + 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 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': + 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) + return + + const expanded = this.getNodeExpanded(n.UUID) + if (expanded && n.hasChildren()) { + this.setNodeExpanded(n, false) + return + } + + if (n.isFirstSibling() && n.getParent().UUID !== ROOT_NODE) { + await _notes2.current.goToNode(n.getParent()?.UUID, true, true) + return + } + + const siblingBefore = n.getSiblingBefore() + const siblingExpanded = this.getNodeExpanded(siblingBefore?.UUID) + if (siblingBefore !== null && siblingExpanded && siblingBefore.hasChildren()) { + const siblingAbove = this.getLastExpandedNode(siblingBefore) + await _notes2.current.goToNode(siblingAbove?.UUID, true, true) + return + } + + await _notes2.current.goToNode(n.getSiblingBefore()?.UUID, true, true) + }//}}} + async navigateRight(n) {//{{{ + if (n === null) + return + + const siblingAfter = n.getSiblingAfter() + const expanded = this.getNodeExpanded(n.UUID) + + if (!expanded && n.hasChildren()) { + this.setNodeExpanded(n, true) + return + } + + if (expanded && n.hasChildren()) { + await _notes2.current.goToNode(n.Children[0]?.UUID, true, true) + return + } + + if (n.isLastSibling()) { + const nextNode = this.getParentWithNextSibling(n) + await _notes2.current.goToNode(nextNode?.UUID, true, true) + return + } + + await _notes2.current.goToNode(n.getSiblingAfter()?.UUID, true, true) + }//}}} + async navigateUp(n) {//{{{ + if (n === null) + 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() + if (parent?.UUID === ROOT_NODE) + return + await _notes2.current.goToNode(parent?.UUID, true, true) + return + } + + if (siblingBefore !== null && siblingExpanded && siblingBefore.hasChildren()) { + await _notes2.current.goToNode(siblingBefore.Children[siblingBefore.Children.length - 1]?.UUID, true, true) + return + } + + if (siblingBefore) { + await _notes2.current.goToNode(siblingBefore.UUID, true, true) + return + } + }//}}} + async navigateDown(n) {//{{{ + if (n === null) + 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) + await _notes2.current.goToNode(wantedNode?.UUID, true, true) + return + } + + if (nodeExpanded && n.isLastSibling() && !n.hasChildren()) { + const wantedNode = this.getParentWithNextSibling(n) + await _notes2.current.goToNode(wantedNode?.UUID, true, true) + return + } + + // Node not expanded. Go to this node's next sibling. + // GoToNode will abort if given null. + if (!nodeExpanded || !n.hasChildren()) { + await _notes2.current.goToNode(n.getSiblingAfter()?.UUID, true, true) + return + } + + // Node is expanded. + // Children will be visually beneath this node, if any. + if (nodeExpanded && n.hasChildren()) { + await _notes2.current.goToNode(n.Children[0].UUID, true, true) + return + } + }//}}} + async navigateTop() {//{{{ + const root = await nodeStore.get(ROOT_NODE) + if (root.Children.length === 0) + return + await _notes2.current.goToNode(root.Children[0]?.UUID, true, 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) + await _notes2.current.goToNode(lastnode?.UUID, true, true) + } else + await _notes2.current.goToNode(root.Children[root.Children.length - 1]?.UUID, true, true) + }//}}} +} + +class TreeNode extends Component { + constructor(props) {//{{{ + super(props) + this.children_populated = signal(false) + if (this.props.node.Level === 0 || this.props.tree.getNodeExpanded(this.props.node.UUID)) + this.fetchChildren() + }//}}} + render({ tree, node, parent }) {//{{{ + // Fetch the next level of children if the parent tree node is expanded and our children thus will be visible. + const selected = tree.isSelected(node) ? 'selected' : '' + + if (!this.children_populated.value && tree.getNodeExpanded(parent?.props.node.UUID)) + this.fetchChildren() + + const children = node.Children.map(node => { + tree.treeNodeComponents[node.UUID] = createRef() + return html`<${TreeNode} key=${`treenode_${node.UUID}`} tree=${tree} node=${node} parent=${this} ref=${tree.treeNodeComponents[node.UUID]} selected=${node.UUID === tree.props.app.startNode?.UUID} />` + }) + + let expandImg = '' + if (node.Children.length === 0) + expandImg = html`` + else { + if (tree.getNodeExpanded(node.UUID)) + expandImg = html`` + else + expandImg = html`` + } + + return html` +
    +
    { tree.setNodeExpanded(node, !tree.getNodeExpanded(node.UUID)) }}>${expandImg}
    +
    window._notes2.current.goToNode(node.UUID)}>${node.get('Name')}
    +
    ${children}
    +
    ` + }//}}} + async fetchChildren() {//{{{ + await this.props.node.fetchChildren() + this.children_populated.value = true + }//}}} +} + class Op { constructor(id) { this.id = id diff --git a/static/js/tree.mjs b/static/js/tree.mjs index b672742..3732fc5 100644 --- a/static/js/tree.mjs +++ b/static/js/tree.mjs @@ -1,19 +1,14 @@ import { ROOT_NODE } from 'node_store' import { CustomHTMLElement } from './lib/custom_html_element.mjs' -import { Color, Solver } from './lib/css_colorize.mjs' export class N2Tree extends CustomHTMLElement { static {// {{{ this.tmpl = document.createElement('template') this.tmpl.innerHTML = ` - +
    - - + +
    ` @@ -36,7 +31,7 @@ export class N2Tree extends CustomHTMLElement { this.elSync.addEventListener('click', () => _sync.run()) this.elLogo.addEventListener('click', () => _app.goToNode(ROOT_NODE, false, false)) - _mbus.subscribe('NODE_MODIFIED', ({ detail }) => { + _mbus.subscribe('NODE_MODIFIED', ({ detail })=>{ const node = detail.data.node const treenode = this.treeNodeComponents[node.get('UUID')] @@ -48,12 +43,6 @@ export class N2Tree extends CustomHTMLElement { }) this.populateFirstLevel() - - /* XXX - set color */ - let color = new Color(255, 96, 80) - let solver = new Solver(color) - let result = solver.solve() - this.elSettings.style.filter = result.filter }// }}} render() {// {{{ if (this.rendered) diff --git a/static/service_worker.js b/static/service_worker.js index 1717eef..4a5f71d 100644 --- a/static/service_worker.js +++ b/static/service_worker.js @@ -24,6 +24,7 @@ const CACHED_ASSETS = [ '/js/{{ .VERSION }}/crypto.mjs', '/js/{{ .VERSION }}/key.mjs', '/js/{{ .VERSION }}/lib/custom_html_element.mjs', + '/js/{{ .VERSION }}/lib/fullcalendar.min.js', '/js/{{ .VERSION }}/lib/node_modules/marked/lib/marked.esm.js', '/js/{{ .VERSION }}/lib/node_modules/marked-token-position/lib/index.esm.js', '/js/{{ .VERSION }}/lib/sjcl.js',