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 />
|
<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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
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;
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue