From 2f9b1925dcdfa7e8712301c90acb5ebb2247032c Mon Sep 17 00:00:00 2001 From: Joscha Date: Wed, 12 Feb 2025 00:08:45 +0100 Subject: [PATCH] Refactor ui state Now with more mode! --- gdn-app/src/App.vue | 4 +- gdn-app/src/components/CNote.vue | 83 ++++++---------------- gdn-app/src/stores/ui.ts | 117 +++++++++++++++++++++++++------ 3 files changed, 119 insertions(+), 85 deletions(-) diff --git a/gdn-app/src/App.vue b/gdn-app/src/App.vue index 8d6c0ca..3d13ced 100644 --- a/gdn-app/src/App.vue +++ b/gdn-app/src/App.vue @@ -9,9 +9,9 @@ const ui = useUiStore(); window.addEventListener("keypress", (ev) => { if (document.activeElement !== document.body) return; - if (ev.key === "Escape") { + if (ev.key === "Escape" && ui.mode === "focus") { const parent = ui.focusPath.parent(); - if (parent) ui.focusPath = parent; + if (parent) ui.focusOn(parent); return; } }); diff --git a/gdn-app/src/components/CNote.vue b/gdn-app/src/components/CNote.vue index 4f77bbd..21fe428 100644 --- a/gdn-app/src/components/CNote.vue +++ b/gdn-app/src/components/CNote.vue @@ -11,7 +11,6 @@ import { RiEditLine, RiPushpinFill, RiPushpinLine, - RiStickyNoteAddLine, } from "@remixicon/vue"; import { computed, ref, watchEffect } from "vue"; import CNoteButton from "./CNoteButton.vue"; @@ -55,40 +54,24 @@ const children = computed(() => { }); const hovering = ref(false); -const mode = ref<"editing" | "creating">(); +const hover = computed(() => hovering.value && !editing.value); const mayOpen = computed(() => children.value.length > 0); const open = computed(() => mayOpen.value && ui.isOpen(path)); -const focused = computed(() => ui.focusPath.eq(path)); const pinned = computed(() => ui.isPinned(segment, parentId)); -const hover = computed(() => hovering.value && mode.value !== "editing"); - -const creating = computed(() => mode.value === "creating"); -const editing = computed(() => mode.value === "editing"); +const focused = computed(() => ui.isFocused(path)); +const editing = computed(() => ui.isEditing(path)); +const insertIndex = computed(() => ui.getInsertIndex(path)); // Ensure we're open if we need to be. watchEffect(() => { - if (forceOpen || creating.value) ui.setOpen(path, true); + if (forceOpen || editing.value) ui.setOpen(path, true); }); -// Ensure only one editor is ever open. -watchEffect(() => { - if (!focused.value) mode.value = undefined; -}); - -// Ensure we're focused when an editor is open. -watchEffect(() => { - if (mode.value) focusOnThis(); -}); - -function focusOnThis(): void { - ui.focusPath = path; -} - function onClick(): void { if (!focused.value) { - focusOnThis(); + ui.focusOn(path); return; } @@ -102,11 +85,11 @@ function onPinButtonClick(): void { function onEditButtonClick(): void { if (!note.value) return; - mode.value = "editing"; + ui.edit(path); } function onEditEditorClose(): void { - mode.value = undefined; + ui.focus(); } function onEditEditorFinish(text: string): void { @@ -115,27 +98,6 @@ function onEditEditorFinish(text: string): void { onEditEditorClose(); } -function onCreateButtonClick(): void { - if (!note.value) return; - mode.value = "creating"; -} - -function onCreateEditorClose(): void { - mode.value = undefined; -} - -function onCreateEditorFinish(text: string): void { - if (!note.value) return; - - const newNote = notes.createNote(text); - note.value.children.push(newNote.id); - - const lastChild = children.value.at(-1); - if (lastChild) ui.focusPath = path.concat(lastChild); - - onCreateEditorClose(); -} - function onMoveButtonClick(): void { ui.pushAnchorId(segment.id); } @@ -190,9 +152,6 @@ function onMoveButtonClick(): void { - - - @@ -209,21 +168,19 @@ function onMoveButtonClick(): void {
- -
- - -
-
- +
+
+
- + +
diff --git a/gdn-app/src/stores/ui.ts b/gdn-app/src/stores/ui.ts index 6757e29..7f0df87 100644 --- a/gdn-app/src/stores/ui.ts +++ b/gdn-app/src/stores/ui.ts @@ -2,42 +2,62 @@ import { Segment, Path as UiPath } from "@/lib/path"; import { defineStore } from "pinia"; import { computed, ref, watchEffect } from "vue"; -export const useUiStore = defineStore("ui", () => { - const history = ref< - { - anchorId: string; - focusPath: UiPath; - openPaths: Set; - }[] - >([]); +interface HistoryEntry { + anchorId: string; + focusPath: UiPath; + openPaths: Set; +} +type Mode = + | { type: "focus" } + | { type: "edit"; path: UiPath } + | { type: "insert"; path: UiPath; index: number }; + +export const useUiStore = defineStore("ui", () => { + const history = ref([]); + + // Managed by history const _anchorId = ref(); - const anchorId = computed(() => _anchorId.value); - const focusPath = ref(new UiPath()); + const _focusPath = ref(new UiPath()); const openPaths = ref>(new Set()); + const _mode = ref({ type: "focus" }); const pinned = ref<{ segment: Segment; parentId?: string }>(); - // Ensure all nodes on the focusPath are unfolded. + // Ensure the currently focused note is visible. watchEffect(() => { - // The node pointed to by the path itself doesn't need to be unfolded. - for (const ancestor of focusPath.value.ancestors().slice(1)) { - setOpen(ancestor, true); + if (_mode.value.type === "insert") { + for (const ancestor of _mode.value.path.ancestors()) { + setOpen(ancestor, true); + } + } else { + // The node pointed to by the path itself doesn't need to be unfolded. + for (const ancestor of _focusPath.value.ancestors().slice(1)) { + setOpen(ancestor, true); + } } }); + /////////////////////////////////// + // History and anchor management // + /////////////////////////////////// + + const anchorId = computed(() => _anchorId.value); // Getter + function pushAnchorId(id: string): void { if (_anchorId.value) { history.value.push({ anchorId: _anchorId.value, - focusPath: focusPath.value, + focusPath: _focusPath.value, openPaths: openPaths.value, }); } _anchorId.value = id; - focusPath.value = new UiPath(); + _focusPath.value = new UiPath(); openPaths.value = new Set(); + + _mode.value = { type: "focus" }; } function popAnchorId(): void { @@ -47,15 +67,60 @@ export const useUiStore = defineStore("ui", () => { const entry = history.value.pop(); if (entry) { _anchorId.value = entry.anchorId; - focusPath.value = entry.focusPath; + _focusPath.value = entry.focusPath; openPaths.value = entry.openPaths; } else { _anchorId.value = undefined; - focusPath.value = new UiPath(); + _focusPath.value = new UiPath(); openPaths.value = new Set(); } + + _mode.value = { type: "focus" }; } + /////////////////////////////// + // Mode and focus management // + /////////////////////////////// + + const mode = computed(() => _mode.value.type); + const focusPath = computed(() => _focusPath.value); + + function isFocused(path: UiPath): boolean { + return _mode.value.type === "focus" && _focusPath.value.eq(path); + } + + function isEditing(path: UiPath): boolean { + return _mode.value.type === "edit" && _mode.value.path.eq(path); + } + + function getInsertIndex(path: UiPath): number | undefined { + if (_mode.value.type !== "insert") return; + if (!_mode.value.path.eq(path)) return; + if (_mode.value.index < 0) return; + return _mode.value.index; + } + + function focus(): void { + _mode.value = { type: "focus" }; + } + + function focusOn(path: UiPath): void { + focus(); + _focusPath.value = path; + } + + function edit(path: UiPath): void { + _mode.value = { type: "edit", path }; + } + + function insertAt(path: UiPath, index: number): void { + _mode.value = { type: "insert", path, index }; + } + + ////////////////// + // Node folding // + ////////////////// + function isOpen(path: UiPath): boolean { return openPaths.value.has(path.fmt()); } @@ -67,7 +132,7 @@ export const useUiStore = defineStore("ui", () => { openPaths.value.add(path.fmt()); } else if (!value && isOpen(path)) { // Move the focusPath if necessary - if (path.isPrefixOf(focusPath.value)) focusPath.value = path; + if (path.isPrefixOf(_focusPath.value)) _focusPath.value = path; openPaths.value.delete(path.fmt()); } @@ -77,6 +142,10 @@ export const useUiStore = defineStore("ui", () => { setOpen(path, !isOpen(path)); } + ///////////// + // Pinning // + ///////////// + function isPinned(segment: Segment, parentId?: string): boolean { if (!pinned.value) return false; return pinned.value.segment.eq(segment) && pinned.value.parentId === parentId; @@ -92,9 +161,17 @@ export const useUiStore = defineStore("ui", () => { return { anchorId, - focusPath, pushAnchorId, popAnchorId, + isFocused, + isEditing, + getInsertIndex, + mode, + focusPath, + focus, + focusOn, + edit, + insertAt, isOpen, setOpen, toggleOpen,