Store paths of unfolded nodes in ui state
This commit is contained in:
parent
e62f277ee4
commit
dd19497426
7 changed files with 77 additions and 51 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
25
gdn-app/src/util.ts
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue