New useContent composable to store global navigation state. Fixes for Markdown and Canvas

This commit is contained in:
2024-12-30 20:46:24 +01:00
parent 031a51c2fe
commit 9515132659
31 changed files with 320 additions and 220 deletions

View File

@@ -13,7 +13,7 @@ import type { Root } from 'hast';
const { content, proses, filter } = defineProps<{
content: string
proses?: Array<string | Component>
proses?: Record<string, string | Component>
filter?: string
}>();
@@ -39,7 +39,7 @@ watch([node], () => {
while(end < node.value.children.length)
{
end++;
if(heading(node.value.children[end]) && headingRank(node.value.children[end])! >= rank)
if(heading(node.value.children[end]) && headingRank(node.value.children[end])! <= rank)
break;
}
data.value = { ...node.value, children: node.value.children.slice(start, end) };

View File

@@ -1,5 +1,5 @@
<template>
<span :class="{'w-6 h-6 border-4 after:-top-[4px] after:-left-[4px] after:w-6 after:h-6 after:border-4': size === 'normal', 'w-4 h-4 border-2 after:-top-[2px] after:-left-[2px] after:w-4 after:h-4 after:border-2': size === 'small', 'w-12 h-12 border-[6px] after:-top-[6px] after:-left-[6px] after:w-12 after:h-12 after:border-[6px]': size === 'large'}" class="rounded-full border-light-35 dark:border-dark-35 after:block after:relative after:rounded-full after:border-transparent after:border-t-accent-purple after:animate-spin"></span>
<span :class="{'w-6 h-6 after:-top-[4px] after:-left-[4px] after:w-6 after:h-6 after:border-4': size === 'normal', 'w-4 h-4 after:-top-[2px] after:-left-[2px] after:w-4 after:h-4 after:border-2': size === 'small', 'w-12 h-12 after:-top-[6px] after:-left-[6px] after:w-12 after:h-12 after:border-[6px]': size === 'large'}" class="after:block after:relative after:rounded-full after:border-transparent after:border-t-accent-purple after:animate-spin"></span>
</template>
<script setup lang="ts">

View File

@@ -4,7 +4,6 @@ import type { CanvasNode } from '~/types/canvas';
interface Props {
node: CanvasNode;
zoom: number;
}
const props = defineProps<Props>();
@@ -35,16 +34,9 @@ const colors = computed(() => {
<div class="absolute" :style="{transform: `translate(${node.x}px, ${node.y}px)`, width: `${node.width}px`, height: `${node.height}px`, '--canvas-color': node.color?.hex}" :class="{'-z-10': node.type === 'group', 'z-10': node.type !== 'group'}">
<div :class="[colors.border]" class="border-2 bg-light-20 dark:bg-dark-20 overflow-hidden contain-strict w-full h-full flex">
<div class="w-full h-full py-2 px-4 flex !bg-opacity-[0.07]" :class="colors.bg">
<template v-if="node.type === 'group' || zoom > Math.min(0.4, 1000 / size)">
<div v-if="node.text?.length > 0" class="flex items-center">
<MarkdownRenderer :content="node.text" />
</div>
</template>
<template v-else>
<div class="flex flex-1 justify-center items-center bg-light-30 dark:bg-dark-30">
<Icon icon="radix-icons:text-align-left" class="w-8 h-8"/>
</div>
</template>
<div v-if="node.text?.length > 0" class="flex items-center">
<MarkdownRenderer :content="node.text" />
</div>
</div>
</div>
<div v-if="node.type === 'group' && node.label !== undefined" :class="[colors.border]" style="max-width: 100%; font-size: calc(18px * var(--zoom-multiplier))" class="origin-bottom-left tracking-wider border-4 truncate inline-block text-light-100 dark:text-dark-100 absolute bottom-[100%] mb-2 px-2 py-1 font-thin">{{ node.label }}</div>

View File

@@ -1,7 +1,7 @@
<template>
<div class="absolute top-0 left-0 w-full h-full origin-center pointer-events-none *:pointer-events-auto *:select-none">
<div class="absolute top-0 left-0 w-full h-full origin-center pointer-events-none *:pointer-events-auto *:select-none" v-if="canvas">
<div>
<CanvasNode v-for="node of canvas.nodes" :key="node.id" :node="node" :zoom="zoom" />
<CanvasNode v-for="node of canvas.nodes" :key="node.id" :node="node" />
</div>
<template v-for="edge of canvas.edges">
<div :key="edge.id" v-if="edge.label" class="absolute z-10"
@@ -11,17 +11,29 @@
</template>
<svg class="absolute top-0 left-0 overflow-visible w-full h-full origin-top pointer-events-none">
<CanvasEdge v-for="edge of canvas.edges" :key="edge.id"
:path="path(getNode(canvas.nodes, edge.fromNode)!, edge.fromSide, getNode(canvas.nodes, edge.toNode)!, edge.toSide)"
:path="getPath(getNode(canvas.nodes, edge.fromNode)!, edge.fromSide, getNode(canvas.nodes, edge.toNode)!, edge.toSide)"
:color="edge.color" :label="edge.label" />
</svg>
</div>
</template>
<script setup lang="ts">
const { canvas, zoom } = defineProps<{
canvas: CanvasContent
zoom: number
const { path } = defineProps<{
path: string
}>();
const { content, get } = useContent();
const overview = computed(() => content.value.find(e => e.path === path));
const loading = ref(false);
if(overview.value && !overview.value.content)
{
loading.value = true;
await get(path);
loading.value = false;
}
const canvas = computed(() => overview.value && overview.value.content ? JSON.parse(overview.value.content) as CanvasContent : undefined);
</script>
<script lang="ts">
@@ -71,7 +83,7 @@ function posFromDir(e: { minX: number, minY: number, maxX: number, maxY: number
function getBbox(node: CanvasNode): { minX: number, minY: number, maxX: number, maxY: number } {
return { minX: node.x, minY: node.y, maxX: node.x + node.width, maxY: node.y + node.height };
}
function path(from: CanvasNode, fromSide: 'bottom' | 'top' | 'left' | 'right', to: CanvasNode, toSide: 'bottom' | 'top' | 'left' | 'right'): any {
function getPath(from: CanvasNode, fromSide: 'bottom' | 'top' | 'left' | 'right', to: CanvasNode, toSide: 'bottom' | 'top' | 'left' | 'right'): any {
if(from === undefined || to === undefined)
{
return {

View File

@@ -7,21 +7,15 @@ function cancelEvent(e: Event)
<script setup lang="ts">
import { useDrag, useHover, usePinch, useWheel } from '@vueuse/gesture';
import type { CanvasContent } from '~/types/canvas';
import { Icon } from '@iconify/vue/dist/iconify.js';
import { clamp } from '#shared/general.utils';
type CanvasOverview = any;
const { overview } = defineProps<{
overview: CanvasOverview
}>();
const { data: canvas, status } = await useFetch(`/api/file/content/${encodeURIComponent(overview.path)}`, {
transform: (input) => input && input.content ? JSON.parse(input?.content) as CanvasContent : undefined
});
const { path } = defineProps<{ path: string }>();
const { user } = useUserSession();
const isOwner = computed(() => user.value?.id === overview.owner);
const { content } = useContent();
const overview = computed(() => content.value.find(e => e.path === path));
const isOwner = computed(() => user.value?.id === overview.value?.owner);
const dispX = ref(0), dispY = ref(0), minZoom = ref(0.1), zoom = ref(0.5);
const canvasRef = useTemplateRef('canvasRef');
@@ -86,7 +80,7 @@ dark:border-dark-purple
*/
const cancelEvent = (e: Event) => e.preventDefault()
/*const cancelEvent = (e: Event) => e.preventDefault()
useHover(({ hovering }) => {
if (!hovering) {
//@ts-ignore
@@ -101,65 +95,61 @@ useHover(({ hovering }) => {
document.addEventListener('gesturechange', cancelEvent)
}, {
domTarget: canvasRef,
})
});*/
const dragHandler = useDrag(({ delta: [x, y] }: { delta: number[] }) => {
dispX.value += x / zoom.value;
dispY.value += y / zoom.value;
}, {
domTarget: canvasRef,
passive: true,
});
const wheelHandler = useWheel(({ delta: [x, y] }: { delta: number[] }) => {
zoom.value = clamp(zoom.value + y * -0.001, minZoom.value, 3);
}, {
domTarget: canvasRef,
passive: true,
});
const pinchHandler = usePinch(({ offset: [z] }: { offset: number[] }) => {
zoom.value = clamp(z / 2048, minZoom.value, 3);
}, {
domTarget: canvasRef,
passive: true,
});
</script>
<template>
<Loading v-if="status === 'pending'" />
<div v-else-if="canvas">
<div ref="canvasRef" class="absolute top-0 left-0 overflow-hidden w-full h-full touch-none" :style="{ '--zoom-multiplier': (1 / Math.pow(zoom, 0.7)) }">
<div class="flex flex-col absolute sm:top-2 top-10 left-2 z-[35] overflow-hidden gap-4">
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
<Tooltip message="Zoom avant" side="right">
<div @click="zoom = clamp(zoom * 1.1, minZoom, 3)" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
<Icon icon="radix-icons:plus" />
</div>
</Tooltip>
<Tooltip message="Reset" side="right">
<div @click="zoom = 1" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
<Icon icon="radix-icons:reload" />
</div>
</Tooltip>
<Tooltip message="Tout contenir" side="right">
<div @click="reset" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
<Icon icon="radix-icons:corners" />
</div>
</Tooltip>
<Tooltip message="Zoom arrière" side="right">
<div @click="zoom = clamp(zoom * 0.9, minZoom, 3)" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
<Icon icon="radix-icons:minus" />
</div>
</Tooltip>
</div>
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10" v-if="isOwner">
<Tooltip message="Modifier" side="right">
<NuxtLink :to="{ name: 'explore-edit', hash: '#' + overview.path }" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
<Icon icon="radix-icons:pencil-1" />
</NuxtLink>
</Tooltip>
</div>
<div ref="canvasRef" class="absolute top-0 left-0 overflow-hidden w-full h-full touch-none" :style="{ '--zoom-multiplier': (1 / Math.pow(zoom, 0.7)) }">
<div class="flex flex-col absolute sm:top-2 top-10 left-2 z-[35] overflow-hidden gap-4">
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
<Tooltip message="Zoom avant" side="right">
<div @click="zoom = clamp(zoom * 1.1, minZoom, 3)" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
<Icon icon="radix-icons:plus" />
</div>
</Tooltip>
<Tooltip message="Reset" side="right">
<div @click="zoom = 1" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
<Icon icon="radix-icons:reload" />
</div>
</Tooltip>
<Tooltip message="Tout contenir" side="right">
<div @click="reset" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
<Icon icon="radix-icons:corners" />
</div>
</Tooltip>
<Tooltip message="Zoom arrière" side="right">
<div @click="zoom = clamp(zoom * 0.9, minZoom, 3)" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
<Icon icon="radix-icons:minus" />
</div>
</Tooltip>
</div>
<CanvasRenderer :style="{transform: `scale(${zoom}) translate(${dispX}px, ${dispY}px)`}" :canvas="canvas" :zoom="zoom" />
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10" v-if="isOwner">
<Tooltip message="Modifier" side="right">
<NuxtLink :to="{ name: 'explore-edit', hash: '#' + path }" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
<Icon icon="radix-icons:pencil-1" />
</NuxtLink>
</Tooltip>
</div>
</div>
<div :style="{transform: `scale(${zoom}) translate(${dispX}px, ${dispY}px)`}" >
<CanvasRenderer :path="path" />
</div>
</div>
</template>

View File

@@ -1,26 +1,38 @@
<script setup lang="ts">
type MarkdownOverview = any;
const { overview } = defineProps<{
overview: MarkdownOverview
const { path } = defineProps<{
path: string
filter?: string,
popover?: boolean
}>();
const { data: content, status } = await useFetch(`/api/file/content/${encodeURIComponent(overview.path)}`, { watch: [overview] });
const { user } = useUserSession();
const isOwner = computed(() => user.value?.id === overview.owner);
const { content, get } = useContent();
const overview = computed(() => content.value.find(e => e.path === path));
const isOwner = computed(() => user.value?.id === overview.value?.owner);
const loading = ref(false);
if(overview.value && !overview.value.content)
{
loading.value = true;
await get(path);
loading.value = false;
}
</script>
<template>
<div class="flex flex-1 justify-start items-start flex-col xl:px-24 md:px-8 px-4 py-6">
<Loading v-if="status === 'pending'" />
<template v-else>
<div class="flex flex-1 flex-row justify-between items-center">
<Loading v-if="loading" />
<template v-else-if="overview">
<div v-if="!popover" class="flex flex-1 flex-row justify-between items-center">
<ProseH1>{{ overview.title }}</ProseH1>
<div class="flex gap-4">
<NuxtLink :href="{ name: 'explore-edit', hash: '#' + overview.path }" v-if="isOwner"><Button>Modifier</Button></NuxtLink>
</div>
</div>
<MarkdownRenderer v-if="content" :content="content.content" />
<MarkdownRenderer v-if="overview.content" :content="overview.content" :filter="filter" />
</template>
<template v-else>
<div><ProseH2>Impossible d'afficher le contenu demandé</ProseH2></div>
</template>
</div>
</template>

View File

@@ -1,16 +1,14 @@
<template>
<NuxtLink class="text-accent-blue inline-flex items-center"
:to="overview ? { name: 'explore-path', params: { path: overview.path }, hash: hash } : href" :class="class">
<HoverCard class="max-w-[600px] max-h-[600px] w-full overflow-auto z-[45]" :class="{'overflow-hidden !p-0': overview?.type === 'canvas'}" @open="load" :disabled="!overview">
<NuxtLink class="text-accent-blue inline-flex items-center" :to="overview ? { name: 'explore-path', params: { path: overview.path }, hash: hash } : href" :class="class">
<HoverCard nuxt-client class="max-w-[600px] max-h-[600px] w-full overflow-auto z-[45]" :class="{'overflow-hidden !p-0': overview?.type === 'canvas'}" :disabled="!overview">
<template #content>
<Loading v-if="loading" />
<MarkdownRenderer v-else-if="overview?.type === 'markdown'" class="px-10" :content="content!.content" :filter="hash.substring(1)" />
<Canvas v-else-if="overview?.type === 'canvas'" class="w-[600px] h-[600px] relative" :canvas="JSON.parse(content!.content)" />
<Markdown v-if="overview?.type === 'markdown'" class="!px-10" :path="pathname" :filter="hash.substring(1)" popover />
<template v-else-if="overview?.type === 'canvas'"><div class="w-[600px] h-[600px] relative"><Canvas :path="pathname" /></div></template>
</template>
<template #default>
<span>
<slot v-bind="$attrs"></slot>
<Icon class="w-4 h-4 inline-block" v-if="overview && overview.type !== 'markdown'" :icon="iconByType[overview.type]" />
</template>
</span>
</HoverCard>
</NuxtLink>
</template>
@@ -25,37 +23,8 @@ const { href } = defineProps<{
class?: string
}>();
const { hash, pathname, protocol } = parseURL(href);
const overview = ref<{
path: string;
owner: number;
title: string;
type: "file" | "folder" | "markdown" | "canvas";
navigable: boolean;
private: boolean;
order: number;
visit: number;
}>(), content = ref<{
content: string;
}>(), loading = ref(false), fetched = ref(false);
const { hash, pathname } = parseURL(href);
if(!!pathname && !protocol)
{
try {
overview.value = await $fetch(`/api/file/overview/${encodeURIComponent(pathname)}`);
} catch(e) {}
}
async function load()
{
if(fetched.value === true)
return;
fetched.value = true;
loading.value = true;
try {
content.value = await $fetch(`/api/file/content/${encodeURIComponent(pathname)}`);
} catch(e) { }
loading.value = false;
}
const { content } = useContent();
const overview = computed(() => content.value.find(e => e.path === pathname));
</script>