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 @@ @@ -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 @@ @@ -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;
}
};
</script>
<!-- This component is recursively structured. The base case is single block of content. -->
@ -83,36 +139,38 @@ @@ -83,36 +139,38 @@
>
<!-- Zettel base case -->
{#if orderedChildren.length === 0 || depth >= 4}
{#if isEditing}
<form class='w-full'>
<Textarea class='textarea-leather w-full' bind:value={currentContent}>
<div slot='footer' class='flex space-x-2 justify-end'>
<Button
type='reset'
class='btn-leather min-w-fit'
size='sm'
outline
on:click={() => toggleEditing(rootId, false)}
>
Cancel
</Button>
<Button
type='submit'
class='btn-leather min-w-fit'
size='sm'
solid
on:click={() => toggleEditing(rootId, true)}
>
Save
</Button>
</div>
</Textarea>
</form>
{:else}
<P firstupper={isSectionStart}>
{@html currentContent}
</P>
{/if}
{#key updateCount}
{#if isEditing}
<form class='w-full'>
<Textarea class='textarea-leather w-full' bind:value={currentContent}>
<div slot='footer' class='flex space-x-2 justify-end'>
<Button
type='reset'
class='btn-leather min-w-fit'
size='sm'
outline
on:click={() => toggleEditing(rootId, false)}
>
Cancel
</Button>
<Button
type='submit'
class='btn-leather min-w-fit'
size='sm'
solid
on:click={() => toggleEditing(rootId, true)}
>
Save
</Button>
</div>
</Textarea>
</form>
{:else}
<P firstupper={isSectionStart}>
{@html currentContent}
</P>
{/if}
{/key}
{:else}
<div class='flex flex-col space-y-2'>
{#if isEditing}
@ -130,26 +188,34 @@ @@ -130,26 +188,34 @@
</Heading>
{/if}
<!-- Recurse on child indices and zettels -->
{#each orderedChildren as id, index}
<svelte:self
rootId={id}
depth={depth + 1}
{allowEditing}
isSectionStart={index === 0}
on:cursorcapture={handleChildCursorCaptured}
on:cursorrelease={handleChildCursorReleased}
/>
{/each}
{#key subtreeUpdateCount}
{#each orderedChildren as id, index}
<svelte:self
rootId={id}
parentId={rootId}
depth={depth + 1}
{allowEditing}
isSectionStart={index === 0}
bind:needsUpdate={subtreeNeedsUpdate}
on:cursorcapture={handleChildCursorCaptured}
on:cursorrelease={handleChildCursorReleased}
/>
{/each}
{/key}
</div>
{/if}
{#if allowEditing}
{#if allowEditing && depth > 0}
<div class={`flex flex-col space-y-2 justify-start ${buttonsVisible ? 'visible' : 'invisible'}`}>
<Button class='btn-leather' size='sm' outline>
<CaretUpSolid />
</Button>
<Button class='btn-leather' size='sm' outline>
<CaretDownSolid />
</Button>
{#if hasPreviousSibling && parentId}
<Button class='btn-leather' size='sm' outline on:click={() => moveUp(rootId, parentId)}>
<CaretUpSolid />
</Button>
{/if}
{#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)}>
<EditOutline />
</Button>

202
src/lib/parser.ts

@ -251,17 +251,178 @@ export default class Pharos { @@ -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 { @@ -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 { @@ -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
}

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

@ -3,12 +3,21 @@ @@ -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++;
}
}
</script>
<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'>
<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>
</div>

Loading…
Cancel
Save