Refactor ui state
Now with more mode!
This commit is contained in:
parent
4710f19b1e
commit
2f9b1925dc
3 changed files with 119 additions and 85 deletions
|
|
@ -9,9 +9,9 @@ const ui = useUiStore();
|
||||||
window.addEventListener("keypress", (ev) => {
|
window.addEventListener("keypress", (ev) => {
|
||||||
if (document.activeElement !== document.body) return;
|
if (document.activeElement !== document.body) return;
|
||||||
|
|
||||||
if (ev.key === "Escape") {
|
if (ev.key === "Escape" && ui.mode === "focus") {
|
||||||
const parent = ui.focusPath.parent();
|
const parent = ui.focusPath.parent();
|
||||||
if (parent) ui.focusPath = parent;
|
if (parent) ui.focusOn(parent);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import {
|
||||||
RiEditLine,
|
RiEditLine,
|
||||||
RiPushpinFill,
|
RiPushpinFill,
|
||||||
RiPushpinLine,
|
RiPushpinLine,
|
||||||
RiStickyNoteAddLine,
|
|
||||||
} from "@remixicon/vue";
|
} from "@remixicon/vue";
|
||||||
import { computed, ref, watchEffect } from "vue";
|
import { computed, ref, watchEffect } from "vue";
|
||||||
import CNoteButton from "./CNoteButton.vue";
|
import CNoteButton from "./CNoteButton.vue";
|
||||||
|
|
@ -55,40 +54,24 @@ const children = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const hovering = ref(false);
|
const hovering = ref(false);
|
||||||
const mode = ref<"editing" | "creating">();
|
const hover = computed(() => hovering.value && !editing.value);
|
||||||
|
|
||||||
const mayOpen = computed(() => children.value.length > 0);
|
const mayOpen = computed(() => children.value.length > 0);
|
||||||
const open = computed(() => mayOpen.value && ui.isOpen(path));
|
const open = computed(() => mayOpen.value && ui.isOpen(path));
|
||||||
|
|
||||||
const focused = computed(() => ui.focusPath.eq(path));
|
|
||||||
const pinned = computed(() => ui.isPinned(segment, parentId));
|
const pinned = computed(() => ui.isPinned(segment, parentId));
|
||||||
const hover = computed(() => hovering.value && mode.value !== "editing");
|
const focused = computed(() => ui.isFocused(path));
|
||||||
|
const editing = computed(() => ui.isEditing(path));
|
||||||
const creating = computed(() => mode.value === "creating");
|
const insertIndex = computed(() => ui.getInsertIndex(path));
|
||||||
const editing = computed(() => mode.value === "editing");
|
|
||||||
|
|
||||||
// Ensure we're open if we need to be.
|
// Ensure we're open if we need to be.
|
||||||
watchEffect(() => {
|
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 {
|
function onClick(): void {
|
||||||
if (!focused.value) {
|
if (!focused.value) {
|
||||||
focusOnThis();
|
ui.focusOn(path);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,11 +85,11 @@ function onPinButtonClick(): void {
|
||||||
|
|
||||||
function onEditButtonClick(): void {
|
function onEditButtonClick(): void {
|
||||||
if (!note.value) return;
|
if (!note.value) return;
|
||||||
mode.value = "editing";
|
ui.edit(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onEditEditorClose(): void {
|
function onEditEditorClose(): void {
|
||||||
mode.value = undefined;
|
ui.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onEditEditorFinish(text: string): void {
|
function onEditEditorFinish(text: string): void {
|
||||||
|
|
@ -115,27 +98,6 @@ function onEditEditorFinish(text: string): void {
|
||||||
onEditEditorClose();
|
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 {
|
function onMoveButtonClick(): void {
|
||||||
ui.pushAnchorId(segment.id);
|
ui.pushAnchorId(segment.id);
|
||||||
}
|
}
|
||||||
|
|
@ -190,9 +152,6 @@ function onMoveButtonClick(): void {
|
||||||
<CNoteButton :visible="hover" @click.stop="onEditButtonClick">
|
<CNoteButton :visible="hover" @click.stop="onEditButtonClick">
|
||||||
<RiEditLine size="16px" />
|
<RiEditLine size="16px" />
|
||||||
</CNoteButton>
|
</CNoteButton>
|
||||||
<CNoteButton :visible="hover" @click.stop="onCreateButtonClick">
|
|
||||||
<RiStickyNoteAddLine size="16px" />
|
|
||||||
</CNoteButton>
|
|
||||||
<CNoteButton :visible="hover || pinned" :inverted="pinned" @click.stop="onPinButtonClick">
|
<CNoteButton :visible="hover || pinned" :inverted="pinned" @click.stop="onPinButtonClick">
|
||||||
<RiPushpinFill v-if="pinned" size="16px" />
|
<RiPushpinFill v-if="pinned" size="16px" />
|
||||||
<RiPushpinLine v-else size="16px" />
|
<RiPushpinLine v-else size="16px" />
|
||||||
|
|
@ -209,21 +168,19 @@ function onMoveButtonClick(): void {
|
||||||
|
|
||||||
<!-- Children -->
|
<!-- Children -->
|
||||||
<div v-if="open && children.length > 0" class="flex flex-col pl-2">
|
<div v-if="open && children.length > 0" class="flex flex-col pl-2">
|
||||||
<CNote
|
<div v-if="insertIndex === 0" class="flex items-start pl-3">
|
||||||
v-for="child of children"
|
<div class="flex h-6 items-center"><RiAddLine size="16px" /></div>
|
||||||
:key="child.fmt()"
|
<CNoteEditor class="flex-1" />
|
||||||
:path="path.concat(child)"
|
|
||||||
:segment="child"
|
|
||||||
:parent-id="id"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Editor for creating new children -->
|
|
||||||
<div v-if="creating" class="flex items-start pl-3">
|
|
||||||
<div class="flex h-6 items-center">
|
|
||||||
<RiAddLine size="16px" />
|
|
||||||
</div>
|
</div>
|
||||||
<CNoteEditor class="flex-1" @close="onCreateEditorClose" @finish="onCreateEditorFinish" />
|
|
||||||
|
<template v-for="(child, index) of children" :key="child.fmt()">
|
||||||
|
<CNote :path="path.concat(child)" :segment="child" :parent-id="id" />
|
||||||
|
|
||||||
|
<div v-if="insertIndex === index + 1" class="flex items-start pl-3">
|
||||||
|
<div class="flex h-6 items-center"><RiAddLine size="16px" /></div>
|
||||||
|
<CNoteEditor class="flex-1" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -2,42 +2,62 @@ import { Segment, Path as UiPath } from "@/lib/path";
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { computed, ref, watchEffect } from "vue";
|
import { computed, ref, watchEffect } from "vue";
|
||||||
|
|
||||||
export const useUiStore = defineStore("ui", () => {
|
interface HistoryEntry {
|
||||||
const history = ref<
|
anchorId: string;
|
||||||
{
|
focusPath: UiPath;
|
||||||
anchorId: string;
|
openPaths: Set<string>;
|
||||||
focusPath: UiPath;
|
}
|
||||||
openPaths: Set<string>;
|
|
||||||
}[]
|
|
||||||
>([]);
|
|
||||||
|
|
||||||
|
type Mode =
|
||||||
|
| { type: "focus" }
|
||||||
|
| { type: "edit"; path: UiPath }
|
||||||
|
| { type: "insert"; path: UiPath; index: number };
|
||||||
|
|
||||||
|
export const useUiStore = defineStore("ui", () => {
|
||||||
|
const history = ref<HistoryEntry[]>([]);
|
||||||
|
|
||||||
|
// Managed by history
|
||||||
const _anchorId = ref<string>();
|
const _anchorId = ref<string>();
|
||||||
const anchorId = computed(() => _anchorId.value);
|
const _focusPath = ref<UiPath>(new UiPath());
|
||||||
const focusPath = ref<UiPath>(new UiPath());
|
|
||||||
const openPaths = ref<Set<string>>(new Set());
|
const openPaths = ref<Set<string>>(new Set());
|
||||||
|
|
||||||
|
const _mode = ref<Mode>({ type: "focus" });
|
||||||
const pinned = ref<{ segment: Segment; parentId?: string }>();
|
const pinned = ref<{ segment: Segment; parentId?: string }>();
|
||||||
|
|
||||||
// Ensure all nodes on the focusPath are unfolded.
|
// Ensure the currently focused note is visible.
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
// The node pointed to by the path itself doesn't need to be unfolded.
|
if (_mode.value.type === "insert") {
|
||||||
for (const ancestor of focusPath.value.ancestors().slice(1)) {
|
for (const ancestor of _mode.value.path.ancestors()) {
|
||||||
setOpen(ancestor, true);
|
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 {
|
function pushAnchorId(id: string): void {
|
||||||
if (_anchorId.value) {
|
if (_anchorId.value) {
|
||||||
history.value.push({
|
history.value.push({
|
||||||
anchorId: _anchorId.value,
|
anchorId: _anchorId.value,
|
||||||
focusPath: focusPath.value,
|
focusPath: _focusPath.value,
|
||||||
openPaths: openPaths.value,
|
openPaths: openPaths.value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_anchorId.value = id;
|
_anchorId.value = id;
|
||||||
focusPath.value = new UiPath();
|
_focusPath.value = new UiPath();
|
||||||
openPaths.value = new Set();
|
openPaths.value = new Set();
|
||||||
|
|
||||||
|
_mode.value = { type: "focus" };
|
||||||
}
|
}
|
||||||
|
|
||||||
function popAnchorId(): void {
|
function popAnchorId(): void {
|
||||||
|
|
@ -47,15 +67,60 @@ export const useUiStore = defineStore("ui", () => {
|
||||||
const entry = history.value.pop();
|
const entry = history.value.pop();
|
||||||
if (entry) {
|
if (entry) {
|
||||||
_anchorId.value = entry.anchorId;
|
_anchorId.value = entry.anchorId;
|
||||||
focusPath.value = entry.focusPath;
|
_focusPath.value = entry.focusPath;
|
||||||
openPaths.value = entry.openPaths;
|
openPaths.value = entry.openPaths;
|
||||||
} else {
|
} else {
|
||||||
_anchorId.value = undefined;
|
_anchorId.value = undefined;
|
||||||
focusPath.value = new UiPath();
|
_focusPath.value = new UiPath();
|
||||||
openPaths.value = new Set();
|
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 {
|
function isOpen(path: UiPath): boolean {
|
||||||
return openPaths.value.has(path.fmt());
|
return openPaths.value.has(path.fmt());
|
||||||
}
|
}
|
||||||
|
|
@ -67,7 +132,7 @@ export const useUiStore = defineStore("ui", () => {
|
||||||
openPaths.value.add(path.fmt());
|
openPaths.value.add(path.fmt());
|
||||||
} else if (!value && isOpen(path)) {
|
} else if (!value && isOpen(path)) {
|
||||||
// Move the focusPath if necessary
|
// 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());
|
openPaths.value.delete(path.fmt());
|
||||||
}
|
}
|
||||||
|
|
@ -77,6 +142,10 @@ export const useUiStore = defineStore("ui", () => {
|
||||||
setOpen(path, !isOpen(path));
|
setOpen(path, !isOpen(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/////////////
|
||||||
|
// Pinning //
|
||||||
|
/////////////
|
||||||
|
|
||||||
function isPinned(segment: Segment, parentId?: string): boolean {
|
function isPinned(segment: Segment, parentId?: string): boolean {
|
||||||
if (!pinned.value) return false;
|
if (!pinned.value) return false;
|
||||||
return pinned.value.segment.eq(segment) && pinned.value.parentId === parentId;
|
return pinned.value.segment.eq(segment) && pinned.value.parentId === parentId;
|
||||||
|
|
@ -92,9 +161,17 @@ export const useUiStore = defineStore("ui", () => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
anchorId,
|
anchorId,
|
||||||
focusPath,
|
|
||||||
pushAnchorId,
|
pushAnchorId,
|
||||||
popAnchorId,
|
popAnchorId,
|
||||||
|
isFocused,
|
||||||
|
isEditing,
|
||||||
|
getInsertIndex,
|
||||||
|
mode,
|
||||||
|
focusPath,
|
||||||
|
focus,
|
||||||
|
focusOn,
|
||||||
|
edit,
|
||||||
|
insertAt,
|
||||||
isOpen,
|
isOpen,
|
||||||
setOpen,
|
setOpen,
|
||||||
toggleOpen,
|
toggleOpen,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue