Pin notes
This commit is contained in:
parent
de6080c3ad
commit
0b485e6cfe
4 changed files with 50 additions and 11 deletions
|
|
@ -1,13 +1,15 @@
|
||||||
<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 { pathAppend, pathSlice } from "@/util";
|
||||||
import {
|
import {
|
||||||
RiAddLine,
|
RiAddLine,
|
||||||
RiArrowDownSLine,
|
RiArrowDownSLine,
|
||||||
RiArrowRightSLine,
|
RiArrowRightSLine,
|
||||||
RiCornerUpRightLine,
|
RiCornerUpRightLine,
|
||||||
RiEditLine,
|
RiEditLine,
|
||||||
|
RiPushpinFill,
|
||||||
|
RiPushpinLine,
|
||||||
RiStickyNoteAddLine,
|
RiStickyNoteAddLine,
|
||||||
} from "@remixicon/vue";
|
} from "@remixicon/vue";
|
||||||
import { computed, ref, watchEffect } from "vue";
|
import { computed, ref, watchEffect } from "vue";
|
||||||
|
|
@ -53,6 +55,7 @@ const mayOpen = computed(() => children.value.length > 0);
|
||||||
const open = computed(() => mayOpen.value && ui.isOpen(props.path));
|
const open = computed(() => mayOpen.value && ui.isOpen(props.path));
|
||||||
|
|
||||||
const focused = computed(() => ui.focusPath === props.path);
|
const focused = computed(() => ui.focusPath === props.path);
|
||||||
|
const pinned = computed(() => ui.isPinned(props.path));
|
||||||
const hover = computed(() => hovering.value && mode.value !== "editing");
|
const hover = computed(() => hovering.value && mode.value !== "editing");
|
||||||
|
|
||||||
const creating = computed(() => mode.value === "creating");
|
const creating = computed(() => mode.value === "creating");
|
||||||
|
|
@ -86,6 +89,11 @@ function onClick() {
|
||||||
ui.toggleOpen(props.path);
|
ui.toggleOpen(props.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onPinButtonClick() {
|
||||||
|
if (pinned.value) ui.unsetPinned();
|
||||||
|
else ui.setPinned(props.path);
|
||||||
|
}
|
||||||
|
|
||||||
function onEditButtonClick() {
|
function onEditButtonClick() {
|
||||||
if (!note.value) return;
|
if (!note.value) return;
|
||||||
mode.value = "editing";
|
mode.value = "editing";
|
||||||
|
|
@ -168,13 +176,17 @@ function onCreateEditorFinish(text: string) {
|
||||||
<div v-else class="px-1 font-light italic">note not found</div>
|
<div v-else class="px-1 font-light italic">note not found</div>
|
||||||
|
|
||||||
<!-- Controls -->
|
<!-- Controls -->
|
||||||
<div v-if="hover" class="absolute right-0 flex h-6 items-center gap-0.5">
|
<div v-if="hover || pinned" class="absolute right-0 flex h-6 items-center gap-0.5">
|
||||||
<CNoteButton @click.stop="onEditButtonClick">
|
<CNoteButton v-if="hover" @click.stop="onEditButtonClick">
|
||||||
<RiEditLine size="16px" />
|
<RiEditLine size="16px" />
|
||||||
</CNoteButton>
|
</CNoteButton>
|
||||||
<CNoteButton @click.stop="onCreateButtonClick">
|
<CNoteButton v-if="hover" @click.stop="onCreateButtonClick">
|
||||||
<RiStickyNoteAddLine size="16px" />
|
<RiStickyNoteAddLine size="16px" />
|
||||||
</CNoteButton>
|
</CNoteButton>
|
||||||
|
<CNoteButton :inverted="pinned" @click.stop="onPinButtonClick">
|
||||||
|
<RiPushpinFill v-if="pinned" size="16px" />
|
||||||
|
<RiPushpinLine v-else size="16px" />
|
||||||
|
</CNoteButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,13 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{
|
||||||
|
inverted?: boolean;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex select-none items-center rounded-sm border bg-white p-0.5 transition hover:scale-110 active:scale-95"
|
class="flex select-none items-center rounded-sm border p-0.5 transition hover:scale-110 active:scale-95"
|
||||||
|
:class="props.inverted ? 'bg-black text-white' : 'bg-white text-black'"
|
||||||
>
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { pathAncestors, pathLiesOn } from "@/util";
|
import { pathAncestors, pathLiesOn, pathSlice } from "@/util";
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { ref, watchEffect } from "vue";
|
import { ref, watchEffect } from "vue";
|
||||||
|
|
||||||
|
|
@ -6,6 +6,7 @@ export const useUiStore = defineStore("ui", () => {
|
||||||
const anchorId = ref<string>();
|
const anchorId = ref<string>();
|
||||||
const focusPath = ref<string>("");
|
const focusPath = ref<string>("");
|
||||||
const openPaths = ref<Set<string>>(new Set());
|
const openPaths = ref<Set<string>>(new Set());
|
||||||
|
const pinned = ref<string>(); // The last two segments of the path
|
||||||
|
|
||||||
// Ensure all nodes on the focusPath are unfolded.
|
// Ensure all nodes on the focusPath are unfolded.
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
|
|
@ -19,27 +20,42 @@ export const useUiStore = defineStore("ui", () => {
|
||||||
return openPaths.value.has(path);
|
return openPaths.value.has(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setOpen(path: string, open: boolean) {
|
function setOpen(path: string, value: boolean) {
|
||||||
// Move the focusPath if necessary
|
// Move the focusPath if necessary
|
||||||
if (!open && isOpen(path) && pathLiesOn(path, focusPath.value)) {
|
if (!value && isOpen(path) && pathLiesOn(path, focusPath.value)) {
|
||||||
focusPath.value = path;
|
focusPath.value = path;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't update openPaths unnecessarily.
|
// Don't update openPaths unnecessarily.
|
||||||
// Just in case vue itself doesn't debounce Set operations.
|
// Just in case vue itself doesn't debounce Set operations.
|
||||||
if (open && !isOpen(path)) openPaths.value.add(path);
|
if (value && !isOpen(path)) openPaths.value.add(path);
|
||||||
else if (!open && isOpen(path)) openPaths.value.delete(path);
|
else if (!value && isOpen(path)) openPaths.value.delete(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleOpen(path: string) {
|
function toggleOpen(path: string) {
|
||||||
setOpen(path, !isOpen(path));
|
setOpen(path, !isOpen(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isPinned(path: string): boolean {
|
||||||
|
return pathSlice(path, -2) === pinned.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPinned(path: string) {
|
||||||
|
pinned.value = pathSlice(path, -2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unsetPinned() {
|
||||||
|
pinned.value = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
anchorId,
|
anchorId,
|
||||||
focusPath,
|
focusPath,
|
||||||
isOpen,
|
isOpen,
|
||||||
setOpen,
|
setOpen,
|
||||||
toggleOpen,
|
toggleOpen,
|
||||||
|
isPinned,
|
||||||
|
setPinned,
|
||||||
|
unsetPinned,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,16 @@ function pathParse(path: string): string[] {
|
||||||
return path.split("/");
|
return path.split("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function pathSlice(path: string, start: number, end?: number): string {
|
||||||
|
return pathString(pathParse(path).slice(start, end));
|
||||||
|
}
|
||||||
|
|
||||||
export function pathAppend(path: string, key: string): string {
|
export function pathAppend(path: string, key: string): string {
|
||||||
return pathString(pathParse(path).concat(key));
|
return pathString(pathParse(path).concat(key));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pathAncestor(path: string): string {
|
export function pathAncestor(path: string): string {
|
||||||
return pathString(pathParse(path).slice(0, -1));
|
return pathSlice(path, 0, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pathAncestors(path: string): string[] {
|
export function pathAncestors(path: string): string[] {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue