Browse Source

Add node movement to Compose view

Currently doesn't work for moving a zettel between two indices.
master
buttercat1791 1 year ago
parent
commit
ad3dfb6bc4
  1. 170
      src/lib/components/Preview.svelte
  2. 202
      src/lib/parser.ts
  3. 13
      src/routes/new/compose/+page.svelte

170
src/lib/components/Preview.svelte

@ -4,26 +4,60 @@
import { CaretDownSolid, CaretUpSolid, EditOutline } from "flowbite-svelte-icons"; import { CaretDownSolid, CaretUpSolid, EditOutline } from "flowbite-svelte-icons";
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
// TODO: Push parser to state to be read on reload.
export let sectionClass: string = ''; export let sectionClass: string = '';
export let isSectionStart: boolean = false; export let isSectionStart: boolean = false;
export let rootId: string; export let rootId: string;
export let parentId: string | null | undefined = null;
export let depth: number = 0; export let depth: number = 0;
export let allowEditing: boolean = false; export let allowEditing: boolean = false;
export let needsUpdate: boolean = false;
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
let isEditing: boolean = false;
let currentContent: string = $parser.getContent(rootId); 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 hasCursor: boolean = false;
let childHasCursor: boolean; let childHasCursor: boolean;
let title = $parser.getIndexTitle(rootId); let hasPreviousSibling: boolean = false;
const orderedChildren = $parser.getOrderedChildIds(rootId); let hasNextSibling: boolean = false;
let subtreeNeedsUpdate: boolean = false;
let updateCount: number = 0;
let subtreeUpdateCount: number = 0;
$: buttonsVisible = hasCursor && !childHasCursor; $: 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) => { const getHeadingTag = (depth: number) => {
switch (depth) { switch (depth) {
case 0: case 0:
@ -71,6 +105,28 @@
isEditing = !editing; 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;
}
};
</script> </script>
<!-- This component is recursively structured. The base case is single block of content. --> <!-- This component is recursively structured. The base case is single block of content. -->
@ -83,36 +139,38 @@
> >
<!-- Zettel base case --> <!-- Zettel base case -->
{#if orderedChildren.length === 0 || depth >= 4} {#if orderedChildren.length === 0 || depth >= 4}
{#if isEditing} {#key updateCount}
<form class='w-full'> {#if isEditing}
<Textarea class='textarea-leather w-full' bind:value={currentContent}> <form class='w-full'>
<div slot='footer' class='flex space-x-2 justify-end'> <Textarea class='textarea-leather w-full' bind:value={currentContent}>
<Button <div slot='footer' class='flex space-x-2 justify-end'>
type='reset' <Button
class='btn-leather min-w-fit' type='reset'
size='sm' class='btn-leather min-w-fit'
outline size='sm'
on:click={() => toggleEditing(rootId, false)} outline
> on:click={() => toggleEditing(rootId, false)}
Cancel >
</Button> Cancel
<Button </Button>
type='submit' <Button
class='btn-leather min-w-fit' type='submit'
size='sm' class='btn-leather min-w-fit'
solid size='sm'
on:click={() => toggleEditing(rootId, true)} solid
> on:click={() => toggleEditing(rootId, true)}
Save >
</Button> Save
</div> </Button>
</Textarea> </div>
</form> </Textarea>
{:else} </form>
<P firstupper={isSectionStart}> {:else}
{@html currentContent} <P firstupper={isSectionStart}>
</P> {@html currentContent}
{/if} </P>
{/if}
{/key}
{:else} {:else}
<div class='flex flex-col space-y-2'> <div class='flex flex-col space-y-2'>
{#if isEditing} {#if isEditing}
@ -130,26 +188,34 @@
</Heading> </Heading>
{/if} {/if}
<!-- Recurse on child indices and zettels --> <!-- Recurse on child indices and zettels -->
{#each orderedChildren as id, index} {#key subtreeUpdateCount}
<svelte:self {#each orderedChildren as id, index}
rootId={id} <svelte:self
depth={depth + 1} rootId={id}
{allowEditing} parentId={rootId}
isSectionStart={index === 0} depth={depth + 1}
on:cursorcapture={handleChildCursorCaptured} {allowEditing}
on:cursorrelease={handleChildCursorReleased} isSectionStart={index === 0}
/> bind:needsUpdate={subtreeNeedsUpdate}
{/each} on:cursorcapture={handleChildCursorCaptured}
on:cursorrelease={handleChildCursorReleased}
/>
{/each}
{/key}
</div> </div>
{/if} {/if}
{#if allowEditing} {#if allowEditing && depth > 0}
<div class={`flex flex-col space-y-2 justify-start ${buttonsVisible ? 'visible' : 'invisible'}`}> <div class={`flex flex-col space-y-2 justify-start ${buttonsVisible ? 'visible' : 'invisible'}`}>
<Button class='btn-leather' size='sm' outline> {#if hasPreviousSibling && parentId}
<CaretUpSolid /> <Button class='btn-leather' size='sm' outline on:click={() => moveUp(rootId, parentId)}>
</Button> <CaretUpSolid />
<Button class='btn-leather' size='sm' outline> </Button>
<CaretDownSolid /> {/if}
</Button> {#if hasNextSibling && parentId}
<Button class='btn-leather' size='sm' outline on:click={() => moveDown(rootId, parentId)}>
<CaretDownSolid />
</Button>
{/if}
<Button class='btn-leather' size='sm' outline on:click={() => toggleEditing(rootId)}> <Button class='btn-leather' size='sm' outline on:click={() => toggleEditing(rootId)}>
<EditOutline /> <EditOutline />
</Button> </Button>

202
src/lib/parser.ts

@ -251,17 +251,178 @@ export default class Pharos {
return event; 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. * Moves an event within the event tree.
* @param dTag The d tag of the event to be moved. * @param dTag The d tag of the event to be moved.
* @param oldParentDTag The d tag of the moved event's current parent. * @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 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. * @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 * @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 * within the tree changes the hash of several events, so the event tree will be regenerated when
* the consumer next invokes `getEvents()`. * 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); const event = this.events.get(dTag);
if (!event) { if (!event) {
throw new Error(`No event found for #d:${dTag}.`); 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}.`); 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); oldParentMap?.delete(dTag);
newParentMap?.add(dTag);
this.shouldUpdateEventTree = true; 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. * 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. // 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 // #endregion
} }

13
src/routes/new/compose/+page.svelte

@ -3,12 +3,21 @@
import { parser } from "$lib/parser"; import { parser } from "$lib/parser";
import { Heading } from "flowbite-svelte"; import { Heading } from "flowbite-svelte";
let treeNeedsUpdate: boolean = false;
let treeUpdateCount: number = 0;
$: {
if (treeNeedsUpdate) {
treeUpdateCount++;
}
}
</script> </script>
<div class='w-full flex justify-center'> <div class='w-full flex justify-center'>
<main class='main-leather flex flex-col space-y-4 max-w-2xl w-full mt-4 mb-4'> <main class='main-leather flex flex-col space-y-4 max-w-2xl w-full mt-4 mb-4'>
<Heading tag='h1' class='h-leather mb-2'>Compose</Heading> <Heading tag='h1' class='h-leather mb-2'>Compose</Heading>
<Preview rootId={$parser.getRootIndexId()} allowEditing={true} /> {#key treeUpdateCount}
<Preview rootId={$parser.getRootIndexId()} allowEditing={true} bind:needsUpdate={treeNeedsUpdate} />
{/key}
</main> </main>
</div> </div>

Loading…
Cancel
Save