Store paths of unfolded nodes in ui state

This commit is contained in:
Joscha 2025-02-07 02:14:09 +01:00
parent e62f277ee4
commit dd19497426
7 changed files with 77 additions and 51 deletions

View file

@ -11,10 +11,10 @@ const ui = useUiStore();
<CNavbar />
<div class="h-full overflow-auto p-1">
<CNote
v-if="ui.anchor"
:noteId="ui.anchor"
:path="[]"
:focusPath="ui.focusPath"
v-if="ui.anchorId"
:noteId="ui.anchorId"
:path="''"
:forceOpen="true"
/>
</div>
</div>

View file

@ -1,10 +1,10 @@
<script setup lang="ts">
import { Note, useNotesStore } from "@/stores/notes";
import { useReposStore } from "@/stores/repos";
import { useUiStore } from "@/stores/ui";
import { RiDeleteBinFill, RiNodeTree, RiSettings3Fill } from "@remixicon/vue";
import CNavbarButton from "./CNavbarButton.vue";
import CNavbarDropdown from "./CNavbarDropdown.vue";
import { useUiStore } from "@/stores/ui";
import { Note, useNotesStore } from "@/stores/notes";
const repos = useReposStore();
const notes = useNotesStore();
@ -29,7 +29,7 @@ function createSomeNotes() {
const root = mkNote("root", n1.id, n2.id, n3.id, n4.id, n5.id, n2.id);
ui.anchor = root.id;
ui.anchorId = root.id;
// Shuffle children of root
root.children = root.children

View file

@ -1,6 +1,7 @@
<script setup lang="ts">
import { useNotesStore } from "@/stores/notes";
import { useUiStore } from "@/stores/ui";
import { pathAppend } from "@/util";
import {
RiArrowDownSLine,
RiArrowRightSLine,
@ -15,8 +16,8 @@ const ui = useUiStore();
const props = defineProps<{
noteId: string;
path: number[]; // From root to here
focusPath?: number[]; // From here to focus
path: string; // From root to here
forceOpen?: boolean;
}>();
const note = computed(() => notes.notes.get(props.noteId));
@ -35,53 +36,34 @@ const children = computed(() => {
return children;
});
const open = ref(false);
const focused = computed(() => props.focusPath?.length === 0);
const open = computed(() => ui.openPaths.has(props.path));
const focused = computed(() => ui.focusPath === props.path);
const creating = ref(false);
// We want to set open to true when we're on the focus path, but then it should
// stay true. Hence a computed() combining open and forceOpen would not suffice.
// Ensure we're open if we need to be.
watchEffect(() => {
open.value; // Ensure we stay open if `open.value = false` is attempted
if (props.focusPath && props.focusPath.length > 0) open.value = true;
if (props.forceOpen || creating.value) {
if (!ui.openPaths.has(props.path)) {
ui.openPaths.add(props.path);
}
}
});
// Abort creating a child node whenever we stop being focused.
// Abort creating whenever we stop being focused.
watchEffect(() => {
if (!focused.value) creating.value = false;
});
// Ensure the creator component is visible.
watchEffect(() => {
if (creating.value) open.value = true;
});
function focusPathFor(index: number): number[] | undefined {
if (!props.focusPath) return undefined;
if (index !== props.focusPath[0]) return undefined;
return props.focusPath.slice(1);
}
function focusOnThis() {
ui.focusPath = props.path.slice();
ui.focusPath = props.path;
}
function toggleOpen() {
if (props.focusPath) {
// We're on the focus path, so the situation is one of these cases:
//
// 1. We're closed and focused, in which case this does nothing.
// 2. We're open and focused, in which case this does nothing.
// 3. We're open and on the focus path, in which case closing will hide the
// actual focus and we should be focused instead.
//
// In any case, we can just...
focusOnThis();
if (open.value) {
ui.openPaths.delete(props.path);
} else {
ui.openPaths.add(props.path);
}
open.value = !open.value;
}
function onClick() {
@ -125,8 +107,7 @@ function onClick() {
v-for="([noteId, key], index) in children"
:key
:note-id
:path="path.concat(index)"
:focusPath="focusPathFor(index)"
:path="pathAppend(path, index)"
/>
</div>

View file

@ -1,7 +1,7 @@
<script setup lang="ts">
import { RiAddLine, RiCheckLine, RiCloseLine } from "@remixicon/vue";
import CNoteButton from "./CNoteButton.vue";
import { onMounted, ref, useTemplateRef } from "vue";
import CNoteButton from "./CNoteButton.vue";
const emit = defineEmits<{
(e: "close"): void;

View file

@ -1,12 +1,32 @@
import { pathAncestors, pathLiesOn } from "@/util";
import { defineStore } from "pinia";
import { ref } from "vue";
import { ref, watch, watchEffect } from "vue";
export const useUiStore = defineStore("ui", () => {
const anchor = ref<string>();
const focusPath = ref<number[]>([1]);
const anchorId = ref<string>();
const focusPath = ref<string>("");
const openPaths = ref<Set<string>>(new Set());
// Ensure all nodes on the focusPath are unfolded.
watchEffect(() => {
// The node pointed to by the path itself doesn't need to be unfolded.
for (const ancestor of pathAncestors(focusPath.value).slice(1)) {
openPaths.value.add(ancestor);
}
});
// Ensure the focusPath is updated when a node that lies on it is folded.
watch(openPaths, (now, old) => {
for (const folded of old.difference(now)) {
if (pathLiesOn(folded, focusPath.value)) {
focusPath.value = folded;
}
}
});
return {
anchor,
anchorId,
focusPath,
openPaths,
};
});

25
gdn-app/src/util.ts Normal file
View file

@ -0,0 +1,25 @@
function pathString(path: number[]): string {
return path.join("/");
}
function pathParse(path: string): number[] {
if (path === "") return [];
return path.split("/").map((it) => Number.parseInt(it));
}
export function pathAppend(path: string, segment: number): string {
return pathString(pathParse(path).concat(segment));
}
export function pathAncestors(path: string): string[] {
const parsedPath = pathParse(path);
const result = [];
for (let i = parsedPath.length; i >= 0; i--) {
result.push(pathString(parsedPath.slice(0, i)));
}
return result;
}
export function pathLiesOn(p1: string, p2: string): boolean {
return pathAncestors(p2).indexOf(p1) >= 0;
}