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 /> <CNavbar />
<div class="h-full overflow-auto p-1"> <div class="h-full overflow-auto p-1">
<CNote <CNote
v-if="ui.anchor" v-if="ui.anchorId"
:noteId="ui.anchor" :noteId="ui.anchorId"
:path="[]" :path="''"
:focusPath="ui.focusPath" :forceOpen="true"
/> />
</div> </div>
</div> </div>

View file

@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { Note, useNotesStore } from "@/stores/notes";
import { useReposStore } from "@/stores/repos"; import { useReposStore } from "@/stores/repos";
import { useUiStore } from "@/stores/ui";
import { RiDeleteBinFill, RiNodeTree, RiSettings3Fill } from "@remixicon/vue"; import { RiDeleteBinFill, RiNodeTree, RiSettings3Fill } from "@remixicon/vue";
import CNavbarButton from "./CNavbarButton.vue"; import CNavbarButton from "./CNavbarButton.vue";
import CNavbarDropdown from "./CNavbarDropdown.vue"; import CNavbarDropdown from "./CNavbarDropdown.vue";
import { useUiStore } from "@/stores/ui";
import { Note, useNotesStore } from "@/stores/notes";
const repos = useReposStore(); const repos = useReposStore();
const notes = useNotesStore(); 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); 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 // Shuffle children of root
root.children = root.children root.children = root.children

View file

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

View file

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

View file

@ -1,12 +1,32 @@
import { pathAncestors, pathLiesOn } from "@/util";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { ref } from "vue"; import { ref, watch, watchEffect } from "vue";
export const useUiStore = defineStore("ui", () => { export const useUiStore = defineStore("ui", () => {
const anchor = ref<string>(); const anchorId = ref<string>();
const focusPath = ref<number[]>([1]); 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 { return {
anchor, anchorId,
focusPath, 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;
}

View file

@ -26,8 +26,8 @@
// Language and Environment // Language and Environment
"jsx": "preserve", "jsx": "preserve",
"lib": ["ES2020", "DOM", "DOM.Iterable"], "lib": ["ESNext", "DOM", "DOM.Iterable"],
"target": "ES2020", "target": "ESNext",
"useDefineForClassFields": true, "useDefineForClassFields": true,
// Completeness // Completeness