You've already forked obsidian-visualiser
Layouts, ProseA rework and PreviewContent creation. Trying to fix hydration by making better SSR.
This commit is contained in:
@@ -43,7 +43,7 @@ import LiveBlockquote from "~/components/prose/ProseBlockquote.vue";
|
||||
|
||||
import { hash } from 'ohash'
|
||||
import { watch, computed } from 'vue'
|
||||
import type { Root } from 'hast';
|
||||
import type { Root, Node } from 'hast';
|
||||
import { diffLines as diff } from 'diff';
|
||||
|
||||
const model = defineModel<string>();
|
||||
@@ -51,55 +51,81 @@ const model = defineModel<string>();
|
||||
const parser = useMarkdown();
|
||||
const key = computed(() => hash(model.value));
|
||||
|
||||
const node = ref<Root>();
|
||||
const node = ref<Root>(), changes = ref();
|
||||
|
||||
watch(model, async (value, old) => {
|
||||
watch(model, update);
|
||||
update(model.value, "");
|
||||
|
||||
async function update(value: string | undefined, old: string | undefined) {
|
||||
if(value && old)
|
||||
{
|
||||
if(node.value)
|
||||
{
|
||||
let content = "", line = 0, pos = -1, len = 0, child;
|
||||
const d = diff(old, value);
|
||||
const children = node.value?.children.filter(e => e.hasOwnProperty('position'));
|
||||
const differences = diff(old, value, {
|
||||
newlineIsToken: true,
|
||||
});
|
||||
|
||||
for(let i = 0; i < d.length; i++)
|
||||
let removeStart = 0, removeEnd = 0; //Character count
|
||||
let addStart = 0, addEnd = 0; //Character count
|
||||
|
||||
const needAdd = differences.find(e => e.added) !== undefined;
|
||||
const needRemove = differences.find(e => e.removed) !== undefined;
|
||||
|
||||
for(const difference of differences)
|
||||
{
|
||||
if(d[i].added) //Nouvelle ligne
|
||||
if(!difference.added && !difference.removed)
|
||||
{
|
||||
const next = d.length > i ? d[i + 1] : undefined;
|
||||
if(pos === -1 && (!next || !next.removed)) //Nouvelle ligne
|
||||
{
|
||||
child = children.filter(e => e.position?.start.line <= line && e.position?.end.line >= line); //Je cherche tout les blocs qui était inclus dans les lignes éditées.
|
||||
if(child.length > 0)
|
||||
{
|
||||
pos = child[0].position?.start.offset ?? 0; //Je pars du premier caractère du bloc
|
||||
len += (child[child.length - 1].position?.end.offset ?? 0) + 1; //Je m'arrete au dernier caractère du bloc + le \n
|
||||
}
|
||||
}
|
||||
len += d[i].value.length; // J'ajoute le nouveau nombre de caractère
|
||||
removeStart += difference.value.length;
|
||||
addStart += difference.value.length;
|
||||
}
|
||||
else if(d[i].removed) //Ancienne ligne
|
||||
else if(difference.added)
|
||||
{
|
||||
child = children.filter(e => e.position?.start.line <= line + 1 && e.position?.end.line >= line + (d[i].count ?? 1)); //Je cherche tout les blocs qui était inclus dans les lignes éditées.
|
||||
if(child.length > 0)
|
||||
{
|
||||
pos = child[0].position?.start.offset ?? 0; //Je pars du premier caractère du bloc
|
||||
len += child[child.length - 1].position?.end.offset ?? 0 + 1; //Je m'arrete au dernier caractère du bloc
|
||||
len -= d[i].value.length; //Je supprime l'ancien nombre de caractère
|
||||
}
|
||||
addEnd = addStart + difference.value.length;
|
||||
}
|
||||
else
|
||||
else if(difference.removed)
|
||||
{
|
||||
line += d[i].count ?? 0;
|
||||
removeEnd = removeStart + difference.value.length;
|
||||
}
|
||||
|
||||
if((!needAdd || addEnd !== 0) && (!needRemove || removeEnd !== 0))
|
||||
break;
|
||||
}
|
||||
|
||||
node.value = parser(value);
|
||||
const oldNodes = getNodes(node.value.children, removeStart - 1, removeEnd + 1);
|
||||
let newNodes;
|
||||
|
||||
if(oldNodes.length === 0)
|
||||
{
|
||||
node.value = parser(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
const newStart = oldNodes[0].position?.start.offset;
|
||||
const newEnd = oldNodes[oldNodes.length - 1].position?.end.offset;
|
||||
|
||||
const lengthDiff = value.length - old.length;
|
||||
|
||||
newNodes = parser(value.substring(newStart ?? 0, (newEnd ?? 0) + lengthDiff));
|
||||
|
||||
const root = node.value;
|
||||
//root.position?.end.offset
|
||||
|
||||
node.value = parser(value);
|
||||
}
|
||||
console.log(node.value);
|
||||
}
|
||||
else
|
||||
{
|
||||
node.value = parser(value);
|
||||
}
|
||||
}
|
||||
}, { immediate: true });
|
||||
else if(value)
|
||||
{
|
||||
node.value = parser(value);
|
||||
}
|
||||
}
|
||||
function getNodes(nodes: Node[], start: number, end: number)
|
||||
{
|
||||
return nodes.filter(e => (e.position?.start.offset ?? 0) <= end && (e.position?.end.offset ?? 0) >= start);
|
||||
}
|
||||
</script>
|
||||
@@ -1,7 +1,10 @@
|
||||
<template>
|
||||
<template
|
||||
v-if="model && model.length > 0">
|
||||
<MarkdownRenderer :key="key" v-if="node" :node="node"></MarkdownRenderer>
|
||||
<Suspense>
|
||||
<MarkdownRenderer #default :key="key" v-if="node" :node="node"></MarkdownRenderer>
|
||||
<template #fallback><div class="loading"></div></template>
|
||||
</Suspense>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { Node, Text, Element, Comment, Root } from 'hast';
|
||||
import { Text as HText, Comment as HComment } from 'vue';
|
||||
import type { RootContent, Root } from 'hast';
|
||||
import { Text, Comment } from 'vue';
|
||||
|
||||
import ProseP from '~/components/prose/ProseP.vue';
|
||||
import ProseA from '~/components/prose/ProseA.vue';
|
||||
@@ -90,30 +90,30 @@ export default defineComponent({
|
||||
render(ctx: any) {
|
||||
const { node, tags } = ctx;
|
||||
|
||||
const div = h('div', null, (node as Root).children.map(e => renderNode(e, tags)).filter(e => !!e));
|
||||
return div;
|
||||
if(!node)
|
||||
return null;
|
||||
|
||||
return h('div', null, {default: () => (node as Root).children.map(e => renderNode(e, tags)).filter(e => !!e)});
|
||||
}
|
||||
});
|
||||
|
||||
function renderNode(node: Node, tags: Record<string, any>): VNode | undefined
|
||||
function renderNode(node: RootContent, tags: Record<string, any>): VNode | undefined
|
||||
{
|
||||
if(node.type === 'text')
|
||||
if(node.type === 'text' && node.value.length > 0 && node.value !== '\n')
|
||||
{
|
||||
const text = node as Text;
|
||||
if(text.value.length > 0 && text.value !== '\n')
|
||||
return h(HText, (node as Text).value);
|
||||
return h(Text, node.value);
|
||||
}
|
||||
else if(node.type === 'comment')
|
||||
else if(node.type === 'comment' && node.value.length > 0 && node.value !== '\n')
|
||||
{
|
||||
const comment = node as Comment;
|
||||
if(comment.value.length > 0 && comment.value !== '\n')
|
||||
return h(HComment, (node as Comment).value);
|
||||
return h(Comment, node.value);
|
||||
}
|
||||
else if(node.type === 'element')
|
||||
{
|
||||
const element = node as Element;
|
||||
|
||||
return h(tags[element.tagName], { ...element.properties, class: element.properties.className }, element.children.map(e => renderNode(e, tags)).filter(e => !!e));
|
||||
return h(tags[node.tagName] ?? node.tagName, { ...node.properties, class: node.properties.className }, {default: () => node.children.map(e => renderNode(e, tags)).filter(e => !!e)});
|
||||
}
|
||||
else if(node.type === 'raw')
|
||||
{
|
||||
console.warn("???");
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
@@ -44,7 +44,7 @@ async function debounced()
|
||||
<input class="search-bar" type="text" placeholder="Recherche" v-model="input" @input="search">
|
||||
</div>
|
||||
</div>
|
||||
<Teleport to="body" v-if="input !== '' && !!pos">
|
||||
<Teleport to="#teleports" v-if="input !== '' && !!pos">
|
||||
<div class="search-results"
|
||||
:style="{top: (pos.bottom + 4) + 'px', left: pos.left + 'px', width: pos.width + 'px'}">
|
||||
<div class="loading" v-if="loading"></div>
|
||||
@@ -1,33 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
function hideLeftPanel(_: Event)
|
||||
{
|
||||
document?.querySelector('.published-container')?.classList.remove('is-left-column-open');
|
||||
}
|
||||
|
||||
const route = useRoute();
|
||||
const showing = ref(false);
|
||||
|
||||
const project = computed(() => parseInt(Array.isArray(route.params.projectId) ? '0' : route.params.projectId));
|
||||
|
||||
const { data: navigation, execute, status, error } = await useFetch(() => `/api/project/${project.value}/navigation`, {
|
||||
immediate: false,
|
||||
});
|
||||
|
||||
watch(route, () => {
|
||||
if(route.params.projectId && project.value !== 0)
|
||||
{
|
||||
showing.value = true;
|
||||
execute();
|
||||
}
|
||||
else
|
||||
{
|
||||
showing.value = false;
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="{'desktop-hidden': !showing}" class="site-body-left-column">
|
||||
<div class="site-body-left-column">
|
||||
<div class="site-body-left-column-inner">
|
||||
<div class="nav-view-outer">
|
||||
<div class="nav-view">
|
||||
76
components/explorer/PreviewContent.client.vue
Normal file
76
components/explorer/PreviewContent.client.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<Teleport to="#teleports">
|
||||
<div class="popover hover-popover" :class="{'is-loaded': pending === false && content !== ''}" :style="{'top': (pos?.y ?? 0) - 4, 'left': (pos?.y ?? 0) + 4}">
|
||||
<template v-if="display">
|
||||
<div v-if="pending" class="loading"></div>
|
||||
<div class="markdown-embed" v-else-if="content !== ''">
|
||||
<div class="markdown-preview-view markdown-rendered">
|
||||
<div class="markdown-embed-content">
|
||||
<h1>{{ title }}</h1>
|
||||
<Markdown v-model="content"></Markdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="not-found-container" v-else>
|
||||
<div class="not-found-image"></div>
|
||||
<div class="not-found-title">Impossible d'afficher (ou vide)</div>
|
||||
<div class="not-found-description">Cette page est actuellement vide ou impossible à traiter</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</Teleport>
|
||||
<span ref="el" @mouseenter="show" @mouseleave="hide">
|
||||
<slot></slot>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDebounceFn as debounce } from '@vueuse/core';
|
||||
|
||||
const props = defineProps({
|
||||
project: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
path: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
anchor: {
|
||||
type: String,
|
||||
required: false,
|
||||
}
|
||||
})
|
||||
const content = ref(''), title = ref(''), display = ref(false), pending = ref(false);
|
||||
const el = ref(), pos = ref<DOMRect>();
|
||||
watch(display, () => display.value && !pending.value && content.value === '' && fetch());
|
||||
|
||||
onMounted(() => {
|
||||
if(el && el.value)
|
||||
{
|
||||
pos.value = (el.value as HTMLDivElement).getBoundingClientRect();
|
||||
debugger;
|
||||
}
|
||||
})
|
||||
|
||||
async function fetch()
|
||||
{
|
||||
pending.value = true;
|
||||
|
||||
const data = await $fetch(`/api/project/${props.project}/file`, {
|
||||
method: 'get',
|
||||
query: {
|
||||
path: props.path,
|
||||
},
|
||||
});
|
||||
|
||||
pending.value = false;
|
||||
|
||||
if(data && data[0])
|
||||
{
|
||||
content.value = data[0].content;
|
||||
title.value = data[0].title;
|
||||
}
|
||||
}
|
||||
const show = debounce(() => display.value = true, 250), hide = debounce(() => display.value = false, 250);
|
||||
</script>
|
||||
@@ -1,28 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
interface TocItem {
|
||||
text: string
|
||||
id: string
|
||||
children?: TocItem[]
|
||||
|
||||
[key: string]: any
|
||||
}
|
||||
interface Props {
|
||||
link: TocItem;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const hasChildren = computed(() => {
|
||||
return props.link && props.link.children && props.link.children.length > 0 || false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tree-item">
|
||||
<div class="tree-item-self" :class="{'is-clickable': hasChildren}" data-path="{{ props.link.title }}">
|
||||
<NuxtLink no-prefetch class="tree-item-inner" :to="{ hash: '#' + props.link.id, force: true }">{{ props.link.text }}</NuxtLink>
|
||||
</div>
|
||||
<div class="tree-item-children">
|
||||
<TocLink v-if="hasChildren" v-for="link of props.link.children" :link="link" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,103 +1,39 @@
|
||||
<template>
|
||||
<Teleport to="body" v-if="!external && hovered && status !== 'pending'">
|
||||
<div class="popover hover-popover is-loaded" :style="pos" @mouseenter="(e) => showPreview(e, false)"
|
||||
@mouseleave="hidePreview">
|
||||
<template v-if="!!page && !!page[0]">
|
||||
<div class="markdown-embed" v-if="page[0].type === 'Markdown' && page[0].content.length > 0">
|
||||
<div class="markdown-embed-content">
|
||||
<div class="markdown-preview-view markdown-rendered node-insert-event hide-title">
|
||||
<div class="markdown-preview-sizer markdown-preview-section" style="padding-bottom: 0px;">
|
||||
<h1 v-if="page[0]?.title">{{ page[0]?.title }}</h1>
|
||||
<Markdown v-model="page[0].content" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="page[0].type === 'Canvas'" class="canvas-embed is-loaded">
|
||||
<CanvasRenderer :canvas="JSON.parse(page[0].content)" />
|
||||
</div>
|
||||
<div class="markdown-embed" v-else>
|
||||
<div class="not-found-container">
|
||||
<div class="not-found-title">Impossible de prévisualiser</div>
|
||||
<div class="not-found-description">Cliquez sur le lien pour accéder au contenu</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="loading"></div>
|
||||
</div>
|
||||
</Teleport>
|
||||
<NuxtLink :to="{ path, force: true }" :class="class" noPrefetch @mouseenter="(e) => showPreview(e, true)" @mouseleave="hidePreview">
|
||||
<NuxtLink v-if="data && data[0] && status !== 'pending'" :to="{ path: `/explorer/${project}${data[0].path}`, hash: hash }" :class="class">
|
||||
<PreviewContent :project="project" :path="data[0].path" :anchor="hash">
|
||||
<slot v-bind="$attrs"></slot>
|
||||
</PreviewContent>
|
||||
</NuxtLink>
|
||||
<NuxtLink v-else-if="href" :to="{ path: href }" :class="class">
|
||||
<slot v-bind="$attrs"></slot>
|
||||
</NuxtLink>
|
||||
<slot :class="class" v-else v-bind="$attrs"></slot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { parseURL } from 'ufo';
|
||||
|
||||
function showPreview(e: Event, bbox: boolean) {
|
||||
clearTimeout(timeout);
|
||||
if (bbox) {
|
||||
const target = e.currentTarget as HTMLElement;
|
||||
const rect = target?.getBoundingClientRect();
|
||||
const r: any = {};
|
||||
if (rect.bottom + 450 < window.innerHeight)
|
||||
r.top = (rect.bottom + 4) + "px";
|
||||
else
|
||||
r.bottom = (window.innerHeight - rect.top + 4) + "px";
|
||||
if (rect.right + 550 < window.innerWidth)
|
||||
r.left = rect.left + "px";
|
||||
else
|
||||
r.right = (window.innerWidth - rect.right + 4) + "px";
|
||||
pos.value = r;
|
||||
}
|
||||
hovered.value = true;
|
||||
}
|
||||
function hidePreview(e: Event) {
|
||||
timeout = setTimeout(() => hovered.value = false, 300);
|
||||
}
|
||||
|
||||
interface Prop
|
||||
{
|
||||
href?: string;
|
||||
class?: string;
|
||||
project?: number;
|
||||
}
|
||||
const props = defineProps<Prop>();
|
||||
const path = ref(props.href), external = ref(false), hovered = ref(false), pos = ref<any>();
|
||||
let timeout: NodeJS.Timeout;
|
||||
|
||||
if (parseURL(props.href).protocol !== undefined)
|
||||
{
|
||||
external.value = true;
|
||||
}
|
||||
|
||||
let id = ref(props.project);
|
||||
if(id.value === undefined)
|
||||
{
|
||||
id.value = useProject().id.value;
|
||||
}
|
||||
|
||||
const { data: page, status, error, execute } = await useLazyFetch(`/api/project/${id.value}/file`, {
|
||||
query: {
|
||||
search: "%" + parseURL(props.href).pathname,
|
||||
const props = defineProps({
|
||||
href: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
immediate: false,
|
||||
dedupe: 'defer',
|
||||
class: {
|
||||
type: String,
|
||||
required: false,
|
||||
}
|
||||
});
|
||||
|
||||
if(!external.value)
|
||||
{
|
||||
await execute();
|
||||
}
|
||||
|
||||
if(status.value === 'error')
|
||||
{
|
||||
console.error(error.value);
|
||||
}
|
||||
|
||||
if (page.value && page.value[0])
|
||||
{
|
||||
path.value = `/explorer/${id.value}${page.value[0].path}`;
|
||||
console.log(path.value);
|
||||
}
|
||||
const route = useRoute();
|
||||
const { hash, pathname } = parseURL(props.href);
|
||||
const project = computed(() => parseInt(Array.isArray(route.params.projectId) ? '0' : route.params.projectId));
|
||||
const { data, status } = await useFetch(`/api/project/${project.value}/file`, {
|
||||
method: 'GET',
|
||||
query: {
|
||||
search: `%${pathname}`
|
||||
},
|
||||
transform: (data) => data?.map(e => ({ path: e.path })),
|
||||
key: `file:${project.value}:%${pathname}`,
|
||||
dedupe: 'defer'
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user