From ad3dfb6bc48953cbf1ccc722b281232e5e715269 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Sat, 26 Oct 2024 11:54:34 -0500 Subject: [PATCH] Add node movement to Compose view Currently doesn't work for moving a zettel between two indices. --- src/lib/components/Preview.svelte | 170 ++++++++++++++++------- src/lib/parser.ts | 202 +++++++++++++++++++++++++++- src/routes/new/compose/+page.svelte | 13 +- 3 files changed, 326 insertions(+), 59 deletions(-) diff --git a/src/lib/components/Preview.svelte b/src/lib/components/Preview.svelte index a96649c..fe28f9d 100644 --- a/src/lib/components/Preview.svelte +++ b/src/lib/components/Preview.svelte @@ -4,26 +4,60 @@ import { CaretDownSolid, CaretUpSolid, EditOutline } from "flowbite-svelte-icons"; import { createEventDispatcher } from "svelte"; - // TODO: Push parser to state to be read on reload. - export let sectionClass: string = ''; export let isSectionStart: boolean = false; export let rootId: string; + export let parentId: string | null | undefined = null; export let depth: number = 0; export let allowEditing: boolean = false; + export let needsUpdate: boolean = false; const dispatch = createEventDispatcher(); - let isEditing: boolean = false; let currentContent: string = $parser.getContent(rootId); + let title: string | undefined = $parser.getIndexTitle(rootId); + let orderedChildren: string[] = $parser.getOrderedChildIds(rootId); + + let isEditing: boolean = false; let hasCursor: boolean = false; let childHasCursor: boolean; - let title = $parser.getIndexTitle(rootId); - const orderedChildren = $parser.getOrderedChildIds(rootId); + let hasPreviousSibling: boolean = false; + let hasNextSibling: boolean = false; + + let subtreeNeedsUpdate: boolean = false; + let updateCount: number = 0; + let subtreeUpdateCount: number = 0; $: buttonsVisible = hasCursor && !childHasCursor; + $: { + if (needsUpdate) { + updateCount++; + needsUpdate = false; + title = $parser.getIndexTitle(rootId); + currentContent = $parser.getContent(rootId); + } + + if (subtreeNeedsUpdate) { + subtreeUpdateCount++; + subtreeNeedsUpdate = false; + orderedChildren = $parser.getOrderedChildIds(rootId); + } + } + + $: { + if (parentId) { + // Check for previous/next siblings on load + const previousSibling = $parser.getPreviousSibling(rootId, parentId, depth); + const nextSibling = $parser.getNextSibling(rootId, parentId, depth); + + // Hide arrows if no siblings exist + hasPreviousSibling = !!previousSibling[0]; + hasNextSibling = !!nextSibling[0]; + } + } + const getHeadingTag = (depth: number) => { switch (depth) { case 0: @@ -71,6 +105,28 @@ isEditing = !editing; }; + + const moveUp = (rootId: string, parentId: string) => { + // Get the previous sibling and its index + const [prevSiblingId, prevIndex] = $parser.getPreviousSibling(rootId, parentId, depth); + + if (prevSiblingId && prevIndex != null) { + // Move the current event before the previous sibling + $parser.moveEvent(rootId, parentId, parentId, prevIndex); + needsUpdate = true; + } + }; + + const moveDown = (rootId: string, parentId: string) => { + // Get the next sibling and its index + const [nextSiblingId, nextIndex] = $parser.getNextSibling(rootId, parentId, depth); + + if (nextSiblingId && nextIndex != null) { + // Move the current event after the next sibling + $parser.moveEvent(rootId, parentId, parentId, nextIndex + 1); + needsUpdate = true; + } + }; @@ -83,36 +139,38 @@ > {#if orderedChildren.length === 0 || depth >= 4} - {#if isEditing} -
- -
- {:else} -

- {@html currentContent} -

- {/if} + {#key updateCount} + {#if isEditing} +
+ +
+ {:else} +

+ {@html currentContent} +

+ {/if} + {/key} {:else}
{#if isEditing} @@ -130,26 +188,34 @@ {/if} - {#each orderedChildren as id, index} - - {/each} + {#key subtreeUpdateCount} + {#each orderedChildren as id, index} + + {/each} + {/key}
{/if} - {#if allowEditing} + {#if allowEditing && depth > 0}
- - + {#if hasPreviousSibling && parentId} + + {/if} + {#if hasNextSibling && parentId} + + {/if} diff --git a/src/lib/parser.ts b/src/lib/parser.ts index 7891a53..b1fa2ee 100644 --- a/src/lib/parser.ts +++ b/src/lib/parser.ts @@ -251,17 +251,178 @@ export default class Pharos { return event; } + /** + * Gets the immediately preceding sibling of the event with the given d tag. + * @param targetDTag The d tag of the target event. + * @param parentDTag The d tag of the target event's parent. + * @param depth The depth of the target event within the parser tree. + * @returns A tuple containing the d tag of the previous sibling's parent and the index of the + * previous sibling in the parent's children. + */ + getPreviousSibling(targetDTag: string, parentDTag: string, depth: number, maxDepth?: number): [string | null, number | null] { + // TODO: Make sure this gets leaves of a different branch. + // TODO: Try to merge this with getNextSibling(). + maxDepth ??= depth; + // Get siblings as children of the target event's parent. + const siblings = this.indexToChildEventsMap.get(parentDTag); + if (!siblings) { + // If there are no siblings, something has gone wrong. The list of siblings should always + // include at least the target event itself. + throw new Error(`No siblings found for #d:${parentDTag}.`); + } + + const siblingsArray = Array.from(siblings); + const targetIndex = siblingsArray.indexOf(targetDTag); + if (targetIndex === -1) { + return [null, null]; + } + + if (targetIndex === 0) { + // Walk up a level and search for previous siblings + const grandparentDTag = this.getParent(parentDTag); + if (!grandparentDTag) { + return [null, null]; + } + + return this.getPreviousSibling(parentDTag, grandparentDTag, depth - 1, maxDepth); + } + + // Look through previous siblings from right to left + for (let i = targetIndex - 1; i >= 0; i--) { + const siblingDTag = siblingsArray[i]; + + // If this sibling is a leaf node, return it and its index + if (!this.indexToChildEventsMap.has(siblingDTag) || depth === maxDepth) { + return [siblingDTag, i]; + } + + if (depth < maxDepth) { + // Get all children + const children = this.indexToChildEventsMap.get(siblingDTag); + if (children && children.size > 0) { + // Convert to array and get last child + const childrenArray = Array.from(children); + const lastChild = childrenArray[childrenArray.length - 1]; + + // If we are at the same depth as the original target event, then we have found the + // previous sibling. + if (depth === maxDepth) { + return [lastChild, childrenArray.length - 1]; + } + + // If we are above the original target's depth, recursively check the last child. + const [leafDTag, leafIndex] = this.getPreviousSibling( + lastChild, + siblingDTag, + depth + 1, + maxDepth + ); + + if (leafDTag) { + return [leafDTag, leafIndex]; + } + } + } + } + + return [null, null]; + } + + /** + * Gets the immediately following sibling of the event with the given d tag. + * @param targetDTag The d tag of the target event. + * @param parentDTag The d tag of the target event's parent. + * @param depth The depth of the target event within the parser tree. + * @param maxDepth The maximum depth to search for a sibling. + * @returns A tuple containing the d tag of the next sibling's parent and the index of the + * next sibling in the parent's children. + */ + getNextSibling(targetDTag: string, parentDTag: string, depth: number, maxDepth?: number): [string | null, number | null] { + maxDepth ??= depth; + // Get siblings as children of the target event's parent. + const siblings = this.indexToChildEventsMap.get(parentDTag); + if (!siblings) { + // If there are no siblings, something has gone wrong. The list of siblings should always + // include at least the target event itself. + throw new Error(`No siblings found for #d:${parentDTag}.`); + } + + // Convert to array and find target's index + const siblingsArray = Array.from(siblings); + const targetIndex = siblingsArray.indexOf(targetDTag); + if (targetIndex === -1) { + return [null, null]; + } + + if (targetIndex === siblingsArray.length - 1) { + // Walk up a level and search for previous siblings + const grandparentDTag = this.getParent(parentDTag); + if (!grandparentDTag) { + return [null, null]; + } + + return this.getPreviousSibling(parentDTag, grandparentDTag, depth - 1, maxDepth); + } + + // Look through next siblings from left to right + for (let i = targetIndex + 1; i < siblingsArray.length; i++) { + const siblingDTag = siblingsArray[i]; + + // If this sibling is a leaf node, return it and its index + if (!this.indexToChildEventsMap.has(siblingDTag) || depth === maxDepth) { + return [siblingDTag, i]; + } + + if (depth < maxDepth) { + // Get all children + const children = this.indexToChildEventsMap.get(siblingDTag); + if (children && children.size > 0) { + // Convert to array and get first child + const childrenArray = Array.from(children); + const firstChild = childrenArray[0]; + + // If we are at the same depth as the original target event, then we have found the + // next sibling. + if (depth === maxDepth) { + return [firstChild, 0]; + } + + // If we are above the original target's depth, recursively check the first child. + const [leafDTag, leafIndex] = this.getNextSibling( + firstChild, + siblingDTag, + depth + 1, + maxDepth + ); + + if (leafDTag) { + return [leafDTag, leafIndex]; + } + } + } + } + + return [null, null]; + } + /** * Moves an event within the event tree. * @param dTag The d tag of the event to be moved. * @param oldParentDTag The d tag of the moved event's current parent. * @param newParentDTag The d tag of the moved event's new parent. + * @param index The index at which to insert the event to be moved among the children of the new + * parent. * @throws Throws an error if the parameters specify an invalid move. * @remarks Both the old and new parent events must be kind 30040 index events. Moving the event * within the tree changes the hash of several events, so the event tree will be regenerated when * the consumer next invokes `getEvents()`. */ - moveEvent(dTag: string, oldParentDTag: string, newParentDTag: string): void { + moveEvent( + dTag: string, + oldParentDTag: string, + newParentDTag: string, + index?: number + ): void { const event = this.events.get(dTag); if (!event) { throw new Error(`No event found for #d:${dTag}.`); @@ -282,14 +443,21 @@ export default class Pharos { throw new Error(`Event #d:${dTag} is not a child of parent #d:${oldParentDTag}.`); } - // Perform the move. + // Remove the target event from the old parent. oldParentMap?.delete(dTag); - newParentMap?.add(dTag); this.shouldUpdateEventTree = true; - } - // TODO: Add method to update index title. + if (index == null) { + newParentMap?.add(dTag); + return; + } + + // Add the target event to the new parent at the specified index. + const newParentChildren: string[] = Array.from(newParentMap || new Set()); + newParentChildren.splice(index, 0, dTag); + this.indexToChildEventsMap.set(newParentDTag, new Set(newParentChildren)); + } /** * Resets the parser to its initial state, removing any parsed data. @@ -831,6 +999,30 @@ export default class Pharos { // TODO: Add search-based wikilink resolution. + /** + * Gets the d tag of the parent of the event with the given d tag. + * @param dTag The d tag of the target event. + * @returns The d tag of the parent event, or null if the target event does not have a parent. + * @throws An error if the target event does not exist in the parser tree. + */ + private getParent(dTag: string): string | null { + // Check if the event exists in the parser tree. + if (!this.eventIds.has(dTag)) { + throw new Error(`The event indicated by #d:${dTag} does not exist in the parser tree.`); + } + + // Iterate through all the index to child mappings. + // This may be expensive on large trees. + for (const [indexId, childIds] of this.indexToChildEventsMap) { + // If this parent contains our target as a child, we found the parent + if (childIds.has(dTag)) { + return indexId; + } + } + + return null; + } + // #endregion } diff --git a/src/routes/new/compose/+page.svelte b/src/routes/new/compose/+page.svelte index 143ed9d..4d9f473 100644 --- a/src/routes/new/compose/+page.svelte +++ b/src/routes/new/compose/+page.svelte @@ -3,12 +3,21 @@ import { parser } from "$lib/parser"; import { Heading } from "flowbite-svelte"; - + let treeNeedsUpdate: boolean = false; + let treeUpdateCount: number = 0; + + $: { + if (treeNeedsUpdate) { + treeUpdateCount++; + } + }
Compose - + {#key treeUpdateCount} + + {/key}