Implement node movement and copying
This commit is contained in:
parent
75791c4e71
commit
c7452166e0
6 changed files with 260 additions and 9 deletions
|
|
@ -137,6 +137,29 @@ function onInsertEditorFinish(text: string): void {
|
||||||
|
|
||||||
onInsertEditorClose();
|
onInsertEditorClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onInsertEditorMove(): void {
|
||||||
|
if (!ui.pinned) return;
|
||||||
|
if (insertIndex.value === undefined) return;
|
||||||
|
|
||||||
|
if (ui.pinned.parentId) {
|
||||||
|
notes.removeChild(ui.pinned.parentId, ui.pinned.segment);
|
||||||
|
}
|
||||||
|
|
||||||
|
notes.addChild(segment.id, ui.pinned.segment.id, insertIndex.value);
|
||||||
|
|
||||||
|
onInsertEditorClose();
|
||||||
|
ui.unsetPinned();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onInsertEditorCopy(): void {
|
||||||
|
if (!ui.pinned) return;
|
||||||
|
if (insertIndex.value === undefined) return;
|
||||||
|
|
||||||
|
notes.addChild(segment.id, ui.pinned.segment.id, insertIndex.value);
|
||||||
|
|
||||||
|
onInsertEditorClose();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -236,8 +259,11 @@ function onInsertEditorFinish(text: string): void {
|
||||||
<div v-if="open || insertIndex !== undefined" class="flex flex-col pl-2">
|
<div v-if="open || insertIndex !== undefined" class="flex flex-col pl-2">
|
||||||
<CNoteChildEditor
|
<CNoteChildEditor
|
||||||
v-if="insertIndex === 0"
|
v-if="insertIndex === 0"
|
||||||
|
:move-and-copy="ui.pinned !== undefined"
|
||||||
@close="onInsertEditorClose"
|
@close="onInsertEditorClose"
|
||||||
@finish="onInsertEditorFinish"
|
@finish="onInsertEditorFinish"
|
||||||
|
@move="onInsertEditorMove"
|
||||||
|
@copy="onInsertEditorCopy"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<template v-for="(child, index) of children" :key="child.fmt()">
|
<template v-for="(child, index) of children" :key="child.fmt()">
|
||||||
|
|
@ -245,8 +271,11 @@ function onInsertEditorFinish(text: string): void {
|
||||||
|
|
||||||
<CNoteChildEditor
|
<CNoteChildEditor
|
||||||
v-if="insertIndex === index + 1"
|
v-if="insertIndex === index + 1"
|
||||||
|
:move-and-copy="ui.pinned !== undefined"
|
||||||
@close="onInsertEditorClose"
|
@close="onInsertEditorClose"
|
||||||
@finish="onInsertEditorFinish"
|
@finish="onInsertEditorFinish"
|
||||||
|
@move="onInsertEditorMove"
|
||||||
|
@copy="onInsertEditorCopy"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,12 @@
|
||||||
import { RiAddLine } from "@remixicon/vue";
|
import { RiAddLine } from "@remixicon/vue";
|
||||||
import CNoteEditor from "./CNoteEditor.vue";
|
import CNoteEditor from "./CNoteEditor.vue";
|
||||||
|
|
||||||
|
const { moveAndCopy = false } = defineProps<{
|
||||||
|
moveAndCopy?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "close"): void;
|
(e: "close" | "move" | "copy"): void;
|
||||||
(e: "finish", text: string): void;
|
(e: "finish", text: string): void;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -16,8 +20,11 @@ const emit = defineEmits<{
|
||||||
|
|
||||||
<CNoteEditor
|
<CNoteEditor
|
||||||
class="flex-1"
|
class="flex-1"
|
||||||
|
:move-and-copy
|
||||||
@close="() => emit('close')"
|
@close="() => emit('close')"
|
||||||
@finish="(text) => emit('finish', text)"
|
@finish="(text) => emit('finish', text)"
|
||||||
|
@move="() => emit('move')"
|
||||||
|
@copy="() => emit('copy')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RiCheckLine, RiCloseLine } from "@remixicon/vue";
|
import { RiCheckLine, RiCloseLine, RiFileCopyLine, RiFileTransferLine } from "@remixicon/vue";
|
||||||
import { onMounted, ref, useTemplateRef } from "vue";
|
import { onMounted, ref, useTemplateRef } from "vue";
|
||||||
import CNoteButton from "./CNoteButton.vue";
|
import CNoteButton from "./CNoteButton.vue";
|
||||||
|
|
||||||
const { initialText = "" } = defineProps<{
|
const { initialText = "", moveAndCopy = false } = defineProps<{
|
||||||
initialText?: string;
|
initialText?: string;
|
||||||
|
moveAndCopy?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "close"): void;
|
(e: "close" | "move" | "copy"): void;
|
||||||
(e: "finish", text: string): void;
|
(e: "finish", text: string): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|
@ -56,13 +57,17 @@ function onKeyPress(ev: KeyboardEvent): void {
|
||||||
@click.stop
|
@click.stop
|
||||||
></textarea>
|
></textarea>
|
||||||
|
|
||||||
<div class="flex h-6 items-center">
|
<div class="flex h-6 items-center gap-0.5">
|
||||||
|
<CNoteButton v-if="moveAndCopy">
|
||||||
|
<RiFileTransferLine size="16px" @click.stop="emit('move')" />
|
||||||
|
</CNoteButton>
|
||||||
|
<CNoteButton v-if="moveAndCopy">
|
||||||
|
<RiFileCopyLine size="16px" @click.stop="emit('copy')" />
|
||||||
|
</CNoteButton>
|
||||||
|
<div v-if="moveAndCopy" class="w-0.5"></div>
|
||||||
<CNoteButton @click.stop="emit('finish', text)">
|
<CNoteButton @click.stop="emit('finish', text)">
|
||||||
<RiCheckLine size="16px" />
|
<RiCheckLine size="16px" />
|
||||||
</CNoteButton>
|
</CNoteButton>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex h-6 items-center">
|
|
||||||
<CNoteButton @click.stop="emit('close')">
|
<CNoteButton @click.stop="emit('close')">
|
||||||
<RiCloseLine size="16px" />
|
<RiCloseLine size="16px" />
|
||||||
</CNoteButton>
|
</CNoteButton>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Segment } from "@/lib/path";
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
|
|
||||||
|
|
@ -41,10 +42,32 @@ export const useNotesStore = defineStore("notes", () => {
|
||||||
notes.value.clear();
|
notes.value.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addChild(id: string, childId: string, index: number): void {
|
||||||
|
const note = getNote(id);
|
||||||
|
if (!note) return;
|
||||||
|
|
||||||
|
note.children.splice(index, 0, childId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeChild(id: string, segment: Segment): void {
|
||||||
|
const note = getNote(id);
|
||||||
|
if (!note) return;
|
||||||
|
|
||||||
|
let index = note.children.indexOf(segment.id);
|
||||||
|
for (let i = 0; i < segment.iteration; i++) {
|
||||||
|
index = note.children.indexOf(segment.id, index + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < 0) return;
|
||||||
|
note.children.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getNote,
|
getNote,
|
||||||
getParents,
|
getParents,
|
||||||
createNote,
|
createNote,
|
||||||
clearNotes,
|
clearNotes,
|
||||||
|
addChild,
|
||||||
|
removeChild,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
188
gdn-app/src/stores/pinned.ts
Normal file
188
gdn-app/src/stores/pinned.ts
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
import { Segment, Path as UiPath } from "@/lib/path";
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { ref, watchEffect } from "vue";
|
||||||
|
|
||||||
|
interface HistoryEntry {
|
||||||
|
anchorId: string;
|
||||||
|
focusPath: UiPath;
|
||||||
|
openPaths: Set<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mode =
|
||||||
|
| { type: "focus" }
|
||||||
|
| { type: "edit"; path: UiPath }
|
||||||
|
| { type: "insert"; path: UiPath; index: number };
|
||||||
|
|
||||||
|
export const usePinnedStore = defineStore("pinned", () => {
|
||||||
|
const history = ref<HistoryEntry[]>([]);
|
||||||
|
|
||||||
|
// Managed by history
|
||||||
|
const anchorId = ref<string>();
|
||||||
|
const focusPath = ref<UiPath>(new UiPath());
|
||||||
|
const openPaths = ref<Set<string>>(new Set());
|
||||||
|
|
||||||
|
const mode = ref<Mode>({ type: "focus" });
|
||||||
|
const pinned = ref<{ segment: Segment; parentId?: string }>();
|
||||||
|
|
||||||
|
// Ensure the currently focused note is visible.
|
||||||
|
watchEffect(() => {
|
||||||
|
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 //
|
||||||
|
///////////////////////////////////
|
||||||
|
|
||||||
|
function pushAnchorId(id: string): void {
|
||||||
|
if (anchorId.value) {
|
||||||
|
history.value.push({
|
||||||
|
anchorId: anchorId.value,
|
||||||
|
focusPath: focusPath.value,
|
||||||
|
openPaths: openPaths.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
anchorId.value = id;
|
||||||
|
focusPath.value = new UiPath();
|
||||||
|
openPaths.value = new Set();
|
||||||
|
|
||||||
|
mode.value = { type: "focus" };
|
||||||
|
}
|
||||||
|
|
||||||
|
function popAnchorId(): void {
|
||||||
|
// Temporary solution until I implement some UI for anchorId===undefined
|
||||||
|
if (history.value.length === 0) return;
|
||||||
|
|
||||||
|
const entry = history.value.pop();
|
||||||
|
if (entry) {
|
||||||
|
anchorId.value = entry.anchorId;
|
||||||
|
focusPath.value = entry.focusPath;
|
||||||
|
openPaths.value = entry.openPaths;
|
||||||
|
} else {
|
||||||
|
anchorId.value = undefined;
|
||||||
|
focusPath.value = new UiPath();
|
||||||
|
openPaths.value = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
mode.value = { type: "focus" };
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////
|
||||||
|
// Mode and focus management //
|
||||||
|
///////////////////////////////
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOpen(path: UiPath, value: boolean): void {
|
||||||
|
// Don't update openPaths unnecessarily.
|
||||||
|
// Just in case vue itself doesn't debounce Set operations.
|
||||||
|
if (value && !isOpen(path)) {
|
||||||
|
openPaths.value.add(path.fmt());
|
||||||
|
} else if (!value && isOpen(path)) {
|
||||||
|
// Move the focusPath if necessary
|
||||||
|
if (path.isPrefixOf(focusPath.value)) focusPath.value = path;
|
||||||
|
|
||||||
|
openPaths.value.delete(path.fmt());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleOpen(path: UiPath): void {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPinned(segment: Segment, parentId?: string): void {
|
||||||
|
pinned.value = { segment, parentId };
|
||||||
|
}
|
||||||
|
|
||||||
|
function unsetPinned(): void {
|
||||||
|
pinned.value = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
history,
|
||||||
|
anchorId,
|
||||||
|
focusPath,
|
||||||
|
openPaths,
|
||||||
|
mode,
|
||||||
|
pinned,
|
||||||
|
|
||||||
|
// History and anchor management
|
||||||
|
pushAnchorId,
|
||||||
|
popAnchorId,
|
||||||
|
|
||||||
|
// Mode and focus management
|
||||||
|
isFocused,
|
||||||
|
isEditing,
|
||||||
|
getInsertIndex,
|
||||||
|
focus,
|
||||||
|
focusOn,
|
||||||
|
edit,
|
||||||
|
insertAt,
|
||||||
|
|
||||||
|
// Node folding
|
||||||
|
isOpen,
|
||||||
|
setOpen,
|
||||||
|
toggleOpen,
|
||||||
|
|
||||||
|
// Pinning
|
||||||
|
isPinned,
|
||||||
|
setPinned,
|
||||||
|
unsetPinned,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
@ -91,7 +91,6 @@ export const useUiStore = defineStore("ui", () => {
|
||||||
function getInsertIndex(path: UiPath): number | undefined {
|
function getInsertIndex(path: UiPath): number | undefined {
|
||||||
if (mode.value.type !== "insert") return;
|
if (mode.value.type !== "insert") return;
|
||||||
if (!mode.value.path.eq(path)) return;
|
if (!mode.value.path.eq(path)) return;
|
||||||
if (mode.value.index < 0) return;
|
|
||||||
return mode.value.index;
|
return mode.value.index;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue