You've already forked obsidian-visualiser
Big rework to include OPFS API for local edits. Big components rework in vanilla JS to optimize. Unfinished, DO NOT SHIP YET !
This commit is contained in:
@@ -80,7 +80,6 @@ const viewport = computed<Box>(() => {
|
||||
const updateScaleVar = useDebounceFn(() => {
|
||||
if(transformRef.value)
|
||||
{
|
||||
console.log(zoom.value);
|
||||
transformRef.value.style.setProperty('--tw-scale', zoom.value.toString());
|
||||
}
|
||||
if(canvasRef.value)
|
||||
@@ -100,10 +99,19 @@ const historyPos = ref(-1);
|
||||
const historyCursor = computed(() => history.value.length > 0 && historyPos.value > -1 ? history.value[historyPos.value] : undefined);
|
||||
|
||||
watch(props, () => {
|
||||
snapFinder = new SnapFinder(hints, viewport, { gridSize: 512, preferences: canvasSettings.value, threshold: 16, cellSize: 64 })
|
||||
canvas.value.nodes?.forEach((e) => snapFinder.update(e));
|
||||
snapFinder = new SnapFinder(hints, viewport, { gridSize: 512, preferences: canvasSettings.value, threshold: 16, cellSize: 64 });
|
||||
|
||||
canvas.value.nodes?.forEach((e) => {
|
||||
snapFinder.update(e);
|
||||
});
|
||||
|
||||
focusing.value = undefined;
|
||||
editing.value = undefined;
|
||||
|
||||
dispX.value = 0;
|
||||
dispY.value = 0;
|
||||
zoom.value = 0.5;
|
||||
|
||||
history.value = [];
|
||||
historyPos.value = -1;
|
||||
fakeEdge.value = {};
|
||||
@@ -334,10 +342,10 @@ function edit(element: Element)
|
||||
}
|
||||
function createNode(e: MouseEvent)
|
||||
{
|
||||
let box = canvasRef.value?.getBoundingClientRect()!;
|
||||
const centerX = (viewportSize.right.value - viewportSize.left.value) / 2 + viewportSize.left.value, centerY = (viewportSize.bottom.value - viewportSize.top.value) / 2 + viewportSize.top.value;
|
||||
const width = 250, height = 100;
|
||||
const x = (e.layerX / zoom.value) - dispX.value - (width / 2);
|
||||
const y = (e.layerY / zoom.value) - dispY.value - (height / 2);
|
||||
const x = e.layerX / zoom.value - dispX.value - width / 2;
|
||||
const y = e.layerY / zoom.value - dispY.value - height / 2;
|
||||
const node: CanvasNode = { id: getID(16), x, y, width, height, type: 'text' };
|
||||
|
||||
if(!canvas.value.nodes)
|
||||
|
||||
@@ -1,61 +1,205 @@
|
||||
<script lang="ts">
|
||||
import type { Editor, EditorConfiguration, EditorChange } from 'codemirror';
|
||||
import { fromTextArea } from 'hypermd';
|
||||
import { crosshairCursor, Decoration, dropCursor, EditorView, keymap, ViewPlugin, ViewUpdate, WidgetType, type DecorationSet } from '@codemirror/view';
|
||||
import { Annotation, EditorState, RangeValue, SelectionRange, type Range } from '@codemirror/state';
|
||||
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
|
||||
import { bracketMatching, defaultHighlightStyle, foldKeymap, HighlightStyle, indentOnInput, syntaxHighlighting, syntaxTree } from '@codemirror/language';
|
||||
import { search, searchKeymap } from '@codemirror/search';
|
||||
import { closeBrackets, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete';
|
||||
import { lintKeymap } from '@codemirror/lint';
|
||||
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
||||
import { IterMode, Tree } from '@lezer/common';
|
||||
import { tags } from '@lezer/highlight';
|
||||
const External = Annotation.define<boolean>();
|
||||
const Hidden = Decoration.mark({ class: 'hidden' });
|
||||
const Bullet = Decoration.mark({ class: '*:hidden before:absolute before:top-2 before:left-0 before:inline-block before:w-2 before:h-2 before:rounded before:bg-light-40 dark:before:bg-dark-40 relative ps-4' });
|
||||
const Blockquote = Decoration.line({ class: '*:hidden before:block !ps-4 relative before:absolute before:top-0 before:bottom-0 before:left-0 before:w-1 before:bg-none before:bg-light-30 dark:before:bg-dark-30' });
|
||||
|
||||
import '#shared/hypermd.extend';
|
||||
const TagTag = tags.special(tags.content);
|
||||
|
||||
const intersects = (a: {
|
||||
from: number;
|
||||
to: number;
|
||||
}, b: {
|
||||
from: number;
|
||||
to: number;
|
||||
}) => !(a.to < b.from || b.to < a.from);
|
||||
|
||||
const highlight = HighlightStyle.define([
|
||||
{ tag: tags.heading1, class: 'text-5xl pt-4 pb-2 after:hidden' },
|
||||
{ tag: tags.heading2, class: 'text-4xl pt-4 pb-2 ps-1 leading-loose after:hidden' },
|
||||
{ tag: tags.heading3, class: 'text-2xl font-bold pt-1 after:hidden' },
|
||||
{ tag: tags.heading4, class: 'text-xl font-semibold pt-1 after:hidden variant-cap' },
|
||||
{ tag: tags.meta, color: "#404740" },
|
||||
{ tag: tags.link, textDecoration: "underline" },
|
||||
{ tag: tags.heading, textDecoration: "underline", fontWeight: "bold" },
|
||||
{ tag: tags.emphasis, fontStyle: "italic" },
|
||||
{ tag: tags.strong, fontWeight: "bold" },
|
||||
{ tag: tags.strikethrough, textDecoration: "line-through" },
|
||||
{ tag: tags.keyword, color: "#708" },
|
||||
{ tag: TagTag, class: 'cursor-default bg-accent-blue bg-opacity-10 hover:bg-opacity-20 text-accent-blue text-sm px-1 ms-1 pb-0.5 rounded-full rounded-se-none border border-accent-blue border-opacity-30' }
|
||||
]);
|
||||
|
||||
class Decorator
|
||||
{
|
||||
static hiddenNodes: string[] = [
|
||||
'HardBreak',
|
||||
'LinkMark',
|
||||
'EmphasisMark',
|
||||
'CodeMark',
|
||||
'CodeInfo',
|
||||
'URL',
|
||||
]
|
||||
decorations: DecorationSet;
|
||||
constructor(view: EditorView)
|
||||
{
|
||||
this.decorations = Decoration.set(this.iterate(syntaxTree(view.state), view.visibleRanges, []), true);
|
||||
}
|
||||
update(update: ViewUpdate)
|
||||
{
|
||||
if(!update.docChanged && !update.viewportChanged && !update.selectionSet)
|
||||
return;
|
||||
|
||||
this.decorations = this.decorations.update({
|
||||
filter: (f, t, v) => false,
|
||||
add: this.iterate(syntaxTree(update.state), update.view.visibleRanges, update.state.selection.ranges),
|
||||
sort: true,
|
||||
});
|
||||
}
|
||||
iterate(tree: Tree, visible: readonly {
|
||||
from: number;
|
||||
to: number;
|
||||
}[], selection: readonly SelectionRange[]): Range<Decoration>[]
|
||||
{
|
||||
const decorations: Range<Decoration>[] = [];
|
||||
|
||||
for (let { from, to } of visible) {
|
||||
tree.iterate({
|
||||
from, to, mode: IterMode.IgnoreMounts,
|
||||
enter: node => {
|
||||
if(node.node.parent && selection.some(e => intersects(e, node.node.parent!)))
|
||||
return true;
|
||||
|
||||
else if(node.name === 'HeaderMark')
|
||||
decorations.push(Hidden.range(node.from, node.to + 1));
|
||||
|
||||
else if(Decorator.hiddenNodes.includes(node.name))
|
||||
decorations.push(Hidden.range(node.from, node.to));
|
||||
|
||||
else if(node.matchContext(['BulletList', 'ListItem']) && node.name === 'ListMark')
|
||||
decorations.push(Bullet.range(node.from, node.to + 1));
|
||||
|
||||
else if(node.matchContext(['Blockquote']))
|
||||
decorations.push(Blockquote.range(node.from, node.from));
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return decorations;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { placeholder, autofocus = false, gutters = true, format = 'd-any' } = defineProps<{
|
||||
const { autofocus = false } = defineProps<{
|
||||
placeholder?: string
|
||||
autofocus?: boolean
|
||||
gutters?: boolean
|
||||
format?: 'hypermd' | 'd-any'
|
||||
}>();
|
||||
const model = defineModel<string>();
|
||||
const editor = ref<Editor>();
|
||||
|
||||
const input = useTemplateRef('input');
|
||||
const editor = useTemplateRef('editor');
|
||||
const view = ref<EditorView>();
|
||||
|
||||
onMounted(() => {
|
||||
if(input.value)
|
||||
if(editor.value)
|
||||
{
|
||||
const e = editor.value = fromTextArea(input.value, {
|
||||
mode: {
|
||||
name: format,
|
||||
hashtag: true,
|
||||
toc: false,
|
||||
math: false,
|
||||
orgModeMarkup: false,
|
||||
tokenTypeOverrides: {
|
||||
hr: "line-HyperMD-hr line-background-HyperMD-hr-bg hr",
|
||||
list1: "list-1",
|
||||
list2: "list-2",
|
||||
list3: "list-3",
|
||||
code: "inline-code",
|
||||
hashtag: "hashtag meta"
|
||||
}
|
||||
},
|
||||
spellcheck: true,
|
||||
autofocus: autofocus,
|
||||
lineNumbers: false,
|
||||
showCursorWhenSelecting: true,
|
||||
indentUnit: 4,
|
||||
autoCloseBrackets: true,
|
||||
foldGutter: gutters,
|
||||
theme: 'custom'
|
||||
} as EditorConfiguration);
|
||||
view.value = new EditorView({
|
||||
doc: model.value,
|
||||
parent: editor.value,
|
||||
extensions: [
|
||||
markdown({
|
||||
base: markdownLanguage,
|
||||
extensions: {
|
||||
defineNodes: [
|
||||
{ name: "Tag", style: TagTag },
|
||||
{ name: "TagMark", style: tags.processingInstruction }
|
||||
],
|
||||
parseInline: [{
|
||||
name: "Tag",
|
||||
parse(cx, next, pos) {
|
||||
if (next != 35 || cx.char(pos + 1) == 35) return -1;
|
||||
let elts = [cx.elt("TagMark", pos, pos + 1)];
|
||||
for (let i = pos + 1; i < cx.end; i++) {
|
||||
let next = cx.char(i);
|
||||
if (next == 35)
|
||||
return cx.addElement(cx.elt("Tag", pos, i + 1, elts.concat(cx.elt("TagMark", i, i + 1))));
|
||||
if (next == 92)
|
||||
elts.push(cx.elt("Escape", i, i++ + 2));
|
||||
if (next == 32 || next == 9 || next == 10 || next == 13) break;
|
||||
}
|
||||
return -1
|
||||
}
|
||||
}],
|
||||
}
|
||||
}),
|
||||
history(),
|
||||
search(),
|
||||
dropCursor(),
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
indentOnInput(),
|
||||
syntaxHighlighting(highlight),
|
||||
bracketMatching(),
|
||||
closeBrackets(),
|
||||
crosshairCursor(),
|
||||
EditorView.lineWrapping,
|
||||
keymap.of([
|
||||
...closeBracketsKeymap,
|
||||
...defaultKeymap,
|
||||
...searchKeymap,
|
||||
...historyKeymap,
|
||||
...foldKeymap,
|
||||
...completionKeymap,
|
||||
...lintKeymap
|
||||
]),
|
||||
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
|
||||
if (viewUpdate.docChanged && !viewUpdate.transactions.some(tr => tr.annotation(External)))
|
||||
{
|
||||
model.value = viewUpdate.state.doc.toString();
|
||||
}
|
||||
}),
|
||||
EditorView.contentAttributes.of({spellcheck: "true"}),
|
||||
ViewPlugin.fromClass(Decorator, {
|
||||
decorations: e => e.decorations,
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
e.setValue(model.value ?? '');
|
||||
e.on('change', (cm: Editor, change: EditorChange) => model.value = cm.getValue());
|
||||
if(autofocus)
|
||||
{
|
||||
view.value.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (view.value)
|
||||
{
|
||||
view.value?.destroy();
|
||||
view.value = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
const value = editor.value?.getValue();
|
||||
if (editor.value && model.value !== value) {
|
||||
editor.value.setValue(model.value ?? '');
|
||||
editor.value.clearHistory();
|
||||
if (model.value === void 0) {
|
||||
return;
|
||||
}
|
||||
const currentValue = view.value ? view.value.state.doc.toString() : "";
|
||||
if (view.value && model.value !== currentValue) {
|
||||
view.value.dispatch({
|
||||
changes: { from: 0, to: currentValue.length, insert: model.value || "" },
|
||||
annotations: [External.of(true)],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -63,72 +207,5 @@ defineExpose({ focus: () => editor.value?.focus() });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="{ 'cancel-gutters': !gutters }" class="flex flex-1 w-full justify-stretch items-stretch !font-sans !text-base">
|
||||
<textarea ref="input" class="hidden"></textarea>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.CodeMirror
|
||||
{
|
||||
@apply bg-transparent;
|
||||
@apply flex-1 h-full;
|
||||
@apply font-sans;
|
||||
|
||||
@apply text-light-100 dark:text-dark-100;
|
||||
}
|
||||
.cancel-gutters .CodeMirror-gutters
|
||||
{
|
||||
@apply hidden;
|
||||
}
|
||||
.CodeMirror-sizer
|
||||
{
|
||||
@apply !px-3;
|
||||
}
|
||||
.cancel-gutters .CodeMirror-sizer
|
||||
{
|
||||
@apply ms-2;
|
||||
}
|
||||
.CodeMirror-gutters
|
||||
{
|
||||
@apply bg-transparent;
|
||||
@apply border-transparent;
|
||||
}
|
||||
.CodeMirror-gutter-wrapper
|
||||
{
|
||||
@apply absolute top-0 bottom-0;
|
||||
@apply flex justify-center items-center;
|
||||
}
|
||||
.CodeMirror-foldmarker
|
||||
{
|
||||
@apply text-light-100;
|
||||
@apply dark:text-dark-100;
|
||||
@apply ps-3;
|
||||
text-shadow: none;
|
||||
}
|
||||
.hmd-inactive-line .cm-formatting-header, .hmd-inactive-line .cm-formatting-link, .hmd-inactive-line .cm-link-has-alias, .hmd-inactive-line .cm-link-alias-pipe
|
||||
{
|
||||
@apply hidden;
|
||||
}
|
||||
.CodeMirror-line
|
||||
{
|
||||
@apply text-base;
|
||||
}
|
||||
.CodeMirror-cursor
|
||||
{
|
||||
@apply border-light-100 dark:border-dark-100;
|
||||
}
|
||||
.CodeMirror-selected
|
||||
{
|
||||
@apply bg-light-35 dark:bg-dark-35;
|
||||
}
|
||||
.HyperMD-list-line-1 {
|
||||
@apply !ps-0;
|
||||
}
|
||||
.HyperMD-list-line-2 {
|
||||
@apply !ps-6;
|
||||
}
|
||||
.HyperMD-list-line-3 {
|
||||
@apply !ps-12;
|
||||
}
|
||||
</style>
|
||||
<div ref="editor" class="flex flex-1 w-full justify-stretch items-stretch py-2 px-1.5 font-sans text-base"></div>
|
||||
</template>
|
||||
@@ -1,27 +1,31 @@
|
||||
<template>
|
||||
<div v-if="content && content.length > 0">
|
||||
<ProsesRenderer #default v-if="data" :node="data" :proses="proses" />
|
||||
</div>
|
||||
<div ref="element"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Component } from 'vue';
|
||||
import { heading } from 'hast-util-heading';
|
||||
import { headingRank } from 'hast-util-heading-rank';
|
||||
import { parseId } from '~/shared/general.util';
|
||||
import type { Root } from 'hast';
|
||||
import { renderMarkdown } from '~/shared/markdown.util';
|
||||
import { tag, a, blockquote, h1, h2, h3, h4, h5, hr, li, small, table, td, th, type Prose } from '~/shared/proses';
|
||||
import { callout } from '../shared/proses';
|
||||
|
||||
const { content, proses, filter } = defineProps<{
|
||||
content: string
|
||||
proses?: Record<string, string | Component>
|
||||
proses?: Record<string, Prose>
|
||||
filter?: string
|
||||
}>();
|
||||
const element = useTemplateRef('element');
|
||||
|
||||
const parser = useMarkdown(), data = ref<Root>();
|
||||
const node = computed(() => content ? parser(content) : undefined);
|
||||
const node = computed(() => content ? parser.parseSync(content) : undefined);
|
||||
watch([node], () => {
|
||||
if(!node.value)
|
||||
{
|
||||
data.value = undefined;
|
||||
return;
|
||||
}
|
||||
else if(!filter)
|
||||
{
|
||||
data.value = node.value;
|
||||
@@ -45,5 +49,32 @@ watch([node], () => {
|
||||
data.value = { ...node.value, children: node.value.children.slice(start, end) };
|
||||
}
|
||||
}
|
||||
|
||||
mount();
|
||||
}, { immediate: true, });
|
||||
|
||||
onMounted(() => {
|
||||
mount();
|
||||
})
|
||||
|
||||
function mount()
|
||||
{
|
||||
if(!element.value || !data.value)
|
||||
return;
|
||||
|
||||
const el = element.value!;
|
||||
|
||||
for(let i = el.childElementCount - 1; i >= 0; i--)
|
||||
{
|
||||
el.removeChild(el.children[i]);
|
||||
}
|
||||
|
||||
el.appendChild(renderMarkdown(data.value, { tag, a, blockquote, callout, h1, h2, h3, h4, h5, hr, li, small, table, td, th, ...proses }));
|
||||
|
||||
const hash = useRouter().currentRoute.value.hash;
|
||||
if(hash.length > 0)
|
||||
{
|
||||
document.getElementById(parseId(hash.substring(1))!)?.scrollIntoView({ behavior: 'instant' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -2,7 +2,7 @@
|
||||
<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="[style.border]" class="outline-0 transition-[outline-width] border-2 bg-light-20 dark:bg-dark-20 w-full h-full hover:outline-4">
|
||||
<div class="w-full h-full py-2 px-4 flex !bg-opacity-[0.07] overflow-auto" :class="style.bg">
|
||||
<div v-if="node.text?.length > 0" class="flex items-center">
|
||||
<div v-if="node.text && node.text.length > 0" class="flex items-center">
|
||||
<MarkdownRenderer :content="node.text" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="absolute" ref="dom" :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 v-if="!editing || node.type === 'group'" style="outline-style: solid;" :class="[style.border, style.outline, { '!outline-4 cursor-move': focusing }]" class="outline-0 transition-[outline-width] border-2 bg-light-20 dark:bg-dark-20 w-full h-full hover:outline-4">
|
||||
<div class="w-full h-full py-2 px-4 flex !bg-opacity-[0.07] overflow-auto" :class="style.bg" @click.left="(e) => { if(node.type !== 'group') selectNode(e) }" @dblclick.left="(e) => { if(node.type !== 'group') editNode(e) }">
|
||||
<div v-if="node.text?.length > 0" class="flex items-center">
|
||||
<div v-if="node.text && node.text.length > 0" class="flex items-center">
|
||||
<MarkdownRenderer :content="node.text" :proses="{ a: FakeA }" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -127,7 +127,7 @@ function dragEdge(e: MouseEvent, direction: Direction) {
|
||||
function unselect() {
|
||||
if(editing.value)
|
||||
{
|
||||
const text = node.type === 'group' ? node.label : node.text;
|
||||
const text = node.type === 'group' ? node.label! : node.text!;
|
||||
|
||||
if(text !== oldText)
|
||||
{
|
||||
|
||||
@@ -1,28 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { type Position } from '#shared/canvas.util';
|
||||
import { hasPermissions } from '~/shared/auth.util';
|
||||
|
||||
const cancelEvent = (e: Event) => e.preventDefault();
|
||||
function center(touches: TouchList): Position
|
||||
{
|
||||
const pos = { x: 0, y: 0 };
|
||||
|
||||
for(const touch of touches)
|
||||
{
|
||||
pos.x += touch.clientX;
|
||||
pos.y += touch.clientY;
|
||||
}
|
||||
|
||||
pos.x /= touches.length;
|
||||
pos.y /= touches.length;
|
||||
return pos;
|
||||
}
|
||||
function distance(touches: TouchList): number
|
||||
{
|
||||
const [A, B] = touches;
|
||||
return Math.hypot(B.clientX - A.clientX, B.clientY - A.clientY);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
stroke-light-red
|
||||
@@ -90,9 +66,8 @@ dark:outline-dark-purple
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import { clamp } from '#shared/general.util';
|
||||
import type { CanvasContent } from '~/types/content';
|
||||
import { Canvas } from '#shared/canvas.util';
|
||||
|
||||
const { path } = defineProps<{
|
||||
path: string
|
||||
@@ -102,183 +77,31 @@ const { user } = useUserSession();
|
||||
const { content, get } = useContent();
|
||||
const overview = computed(() => content.value.find(e => e.path === path) as CanvasContent | undefined);
|
||||
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;
|
||||
}
|
||||
const canvas = computed(() => overview.value && overview.value.content ? overview.value.content : undefined);
|
||||
|
||||
const dispX = ref(0), dispY = ref(0), minZoom = ref(0.1), zoom = ref(0.5);
|
||||
const canvasRef = useTemplateRef('canvasRef'), transformRef = useTemplateRef('transformRef');
|
||||
|
||||
const reset = (_: MouseEvent) => {
|
||||
zoom.value = minZoom.value;
|
||||
|
||||
dispX.value = 0;
|
||||
dispY.value = 0;
|
||||
|
||||
updateTransform();
|
||||
}
|
||||
const element = useTemplateRef('element');
|
||||
|
||||
onMounted(() => {
|
||||
let lastX = 0, lastY = 0, lastDistance = 0;
|
||||
let box = canvasRef.value?.getBoundingClientRect()!;
|
||||
const dragMove = (e: MouseEvent) => {
|
||||
dispX.value = dispX.value - (lastX - e.clientX) / zoom.value;
|
||||
dispY.value = dispY.value - (lastY - e.clientY) / zoom.value;
|
||||
|
||||
lastX = e.clientX;
|
||||
lastY = e.clientY;
|
||||
|
||||
updateTransform();
|
||||
};
|
||||
const dragEnd = (e: MouseEvent) => {
|
||||
window.removeEventListener('mouseup', dragEnd);
|
||||
window.removeEventListener('mousemove', dragMove);
|
||||
};
|
||||
canvasRef.value?.addEventListener('mouseenter', () => {
|
||||
window.addEventListener('wheel', cancelEvent, { passive: false });
|
||||
document.addEventListener('gesturestart', cancelEvent);
|
||||
document.addEventListener('gesturechange', cancelEvent);
|
||||
|
||||
canvasRef.value?.addEventListener('mouseleave', () => {
|
||||
window.removeEventListener('wheel', cancelEvent);
|
||||
document.removeEventListener('gesturestart', cancelEvent);
|
||||
document.removeEventListener('gesturechange', cancelEvent);
|
||||
});
|
||||
})
|
||||
window.addEventListener('resize', () => box = canvasRef.value?.getBoundingClientRect()!);
|
||||
canvasRef.value?.addEventListener('mousedown', (e) => {
|
||||
lastX = e.clientX;
|
||||
lastY = e.clientY;
|
||||
|
||||
window.addEventListener('mouseup', dragEnd, { passive: true });
|
||||
window.addEventListener('mousemove', dragMove, { passive: true });
|
||||
}, { passive: true });
|
||||
canvasRef.value?.addEventListener('wheel', (e) => {
|
||||
if((zoom.value >= 3 && e.deltaY < 0) || (zoom.value <= minZoom.value && e.deltaY > 0))
|
||||
return;
|
||||
|
||||
const diff = Math.exp(e.deltaY * -0.001);
|
||||
const centerX = (box.x + box.width / 2), centerY = (box.y + box.height / 2);
|
||||
const mousex = centerX - e.clientX, mousey = centerY - e.clientY;
|
||||
|
||||
dispX.value = dispX.value - (mousex / (diff * zoom.value) - mousex / zoom.value);
|
||||
dispY.value = dispY.value - (mousey / (diff * zoom.value) - mousey / zoom.value);
|
||||
|
||||
zoom.value = clamp(zoom.value * diff, minZoom.value, 3)
|
||||
|
||||
updateTransform();
|
||||
}, { passive: true });
|
||||
canvasRef.value?.addEventListener('touchstart', (e) => {
|
||||
({ x: lastX, y: lastY } = center(e.touches));
|
||||
|
||||
if(e.touches.length > 1)
|
||||
{
|
||||
lastDistance = distance(e.touches);
|
||||
}
|
||||
|
||||
canvasRef.value?.addEventListener('touchend', touchend, { passive: true });
|
||||
canvasRef.value?.addEventListener('touchcancel', touchcancel, { passive: true });
|
||||
canvasRef.value?.addEventListener('touchmove', touchmove, { passive: true });
|
||||
}, { passive: true });
|
||||
const touchend = (e: TouchEvent) => {
|
||||
if(e.touches.length > 1)
|
||||
{
|
||||
({ x: lastX, y: lastY } = center(e.touches));
|
||||
}
|
||||
|
||||
canvasRef.value?.removeEventListener('touchend', touchend);
|
||||
canvasRef.value?.removeEventListener('touchcancel', touchcancel);
|
||||
canvasRef.value?.removeEventListener('touchmove', touchmove);
|
||||
};
|
||||
const touchcancel = (e: TouchEvent) => {
|
||||
if(e.touches.length > 1)
|
||||
{
|
||||
({ x: lastX, y: lastY } = center(e.touches));
|
||||
}
|
||||
|
||||
canvasRef.value?.removeEventListener('touchend', touchend);
|
||||
canvasRef.value?.removeEventListener('touchcancel', touchcancel);
|
||||
canvasRef.value?.removeEventListener('touchmove', touchmove);
|
||||
};
|
||||
const touchmove = (e: TouchEvent) => {
|
||||
const pos = center(e.touches);
|
||||
dispX.value = dispX.value - (lastX - pos.x) / zoom.value;
|
||||
dispY.value = dispY.value - (lastY - pos.y) / zoom.value;
|
||||
lastX = pos.x;
|
||||
lastY = pos.y;
|
||||
|
||||
if(e.touches.length === 2)
|
||||
{
|
||||
const dist = distance(e.touches);
|
||||
const diff = dist / lastDistance;
|
||||
|
||||
zoom.value = clamp(zoom.value * diff, minZoom.value, 3);
|
||||
}
|
||||
|
||||
updateTransform();
|
||||
};
|
||||
|
||||
updateTransform();
|
||||
mount();
|
||||
});
|
||||
|
||||
function updateTransform()
|
||||
if(overview.value && !overview.value.content)
|
||||
{
|
||||
if(transformRef.value)
|
||||
await get(path);
|
||||
mount();
|
||||
}
|
||||
|
||||
const canvas = computed(() => overview.value && overview.value.content ? overview.value.content : undefined);
|
||||
|
||||
function mount()
|
||||
{
|
||||
if(element.value && canvas.value)
|
||||
{
|
||||
transformRef.value.style.transform = `scale3d(${zoom.value}, ${zoom.value}, 1) translate3d(${dispX.value}px, ${dispY.value}px, 0)`;
|
||||
transformRef.value.style.setProperty('--tw-scale', zoom.value.toString());
|
||||
const c = new Canvas(canvas.value);
|
||||
element.value.appendChild(c.container);
|
||||
c.mount();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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); updateTransform()" 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; updateTransform();" 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 / 1.1, minZoom, 3); updateTransform()" 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>
|
||||
<NuxtLink v-if="overview && isOwner || hasPermissions(user?.permissions ?? [], ['admin', 'editor'])" :to="{ name: 'explore-edit', hash: `#${encodeURIComponent(overview!.path)}` }" class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
|
||||
<Tooltip message="Modifier" side="right">
|
||||
<Icon icon="radix-icons:pencil-1" class="w-8 h-8 p-2" />
|
||||
</Tooltip>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<div ref="transformRef" :style="{
|
||||
'transform-origin': 'center center',
|
||||
}" class="h-full">
|
||||
<div v-if="canvas" class="absolute top-0 left-0 w-full h-full pointer-events-none *:pointer-events-auto *:select-none touch-none">
|
||||
<div>
|
||||
<CanvasNode v-for="node of canvas.nodes" :key="node.id" ref="nodes" :node="node" :zoom="zoom" />
|
||||
</div>
|
||||
<div>
|
||||
<CanvasEdge v-for="edge of canvas.edges" :key="edge.id" ref="edges" :edge="edge" :nodes="canvas.nodes!" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="element"></div>
|
||||
</template>
|
||||
@@ -1,6 +1,6 @@
|
||||
<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 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">
|
||||
<HoverCard nuxt-client class="min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] w-full overflow-auto z-[45]" :class="{'overflow-hidden !p-0': overview?.type === 'canvas'}" :disabled="!overview">
|
||||
<template #content>
|
||||
<Markdown v-if="overview?.type === 'markdown'" class="!px-6" :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>
|
||||
@@ -16,7 +16,7 @@
|
||||
<script setup lang="ts">
|
||||
import { parseURL } from 'ufo';
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import { iconByType } from '#shared/general.util';
|
||||
import { iconByType } from '#shared/content.util';
|
||||
|
||||
const { href } = defineProps<{
|
||||
href: string
|
||||
|
||||
@@ -35,7 +35,6 @@ const defaultCalloutIcon = 'radix-icons:info-circled';
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
const { type, title, fold } = defineProps<{
|
||||
type: string;
|
||||
@@ -43,104 +42,4 @@ const { type, title, fold } = defineProps<{
|
||||
fold?: boolean;
|
||||
}>();
|
||||
const disabled = computed(() => fold === undefined);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.callout[data-type="abstract"],
|
||||
.callout[data-type="summary"],
|
||||
.callout[data-type="tldr"]
|
||||
{
|
||||
@apply bg-light-cyan;
|
||||
@apply dark:bg-dark-cyan;
|
||||
@apply text-light-cyan;
|
||||
@apply dark:text-dark-cyan;
|
||||
}
|
||||
.callout[data-type="info"]
|
||||
{
|
||||
@apply bg-light-blue;
|
||||
@apply dark:bg-dark-blue;
|
||||
@apply text-light-blue;
|
||||
@apply dark:text-dark-blue;
|
||||
}
|
||||
.callout[data-type="todo"]
|
||||
{
|
||||
@apply bg-light-blue;
|
||||
@apply dark:bg-dark-blue;
|
||||
@apply text-light-blue;
|
||||
@apply dark:text-dark-blue;
|
||||
}
|
||||
.callout[data-type="important"]
|
||||
{
|
||||
@apply bg-light-cyan;
|
||||
@apply dark:bg-dark-cyan;
|
||||
@apply text-light-cyan;
|
||||
@apply dark:text-dark-cyan;
|
||||
}
|
||||
.callout[data-type="tip"],
|
||||
.callout[data-type="hint"]
|
||||
{
|
||||
@apply bg-light-cyan;
|
||||
@apply dark:bg-dark-cyan;
|
||||
@apply text-light-cyan;
|
||||
@apply dark:text-dark-cyan;
|
||||
}
|
||||
.callout[data-type="success"],
|
||||
.callout[data-type="check"],
|
||||
.callout[data-type="done"]
|
||||
{
|
||||
@apply bg-light-green;
|
||||
@apply dark:bg-dark-green;
|
||||
@apply text-light-green;
|
||||
@apply dark:text-dark-green;
|
||||
}
|
||||
.callout[data-type="question"],
|
||||
.callout[data-type="help"],
|
||||
.callout[data-type="faq"]
|
||||
{
|
||||
@apply bg-light-orange;
|
||||
@apply dark:bg-dark-orange;
|
||||
@apply text-light-orange;
|
||||
@apply dark:text-dark-orange;
|
||||
}
|
||||
.callout[data-type="warning"],
|
||||
.callout[data-type="caution"],
|
||||
.callout[data-type="attention"]
|
||||
{
|
||||
@apply bg-light-orange;
|
||||
@apply dark:bg-dark-orange;
|
||||
@apply text-light-orange;
|
||||
@apply dark:text-dark-orange;
|
||||
}
|
||||
.callout[data-type="failure"],
|
||||
.callout[data-type="fail"],
|
||||
.callout[data-type="missing"]
|
||||
{
|
||||
@apply bg-light-red;
|
||||
@apply dark:bg-dark-red;
|
||||
@apply text-light-red;
|
||||
@apply dark:text-dark-red;
|
||||
}
|
||||
.callout[data-type="danger"],
|
||||
.callout[data-type="error"]
|
||||
{
|
||||
@apply bg-light-red;
|
||||
@apply dark:bg-dark-red;
|
||||
@apply text-light-red;
|
||||
@apply dark:text-dark-red;
|
||||
}
|
||||
.callout[data-type="bug"]
|
||||
{
|
||||
@apply bg-light-red;
|
||||
@apply dark:bg-dark-red;
|
||||
@apply text-light-red;
|
||||
@apply dark:text-dark-red;
|
||||
}
|
||||
.callout[data-type="example"]
|
||||
{
|
||||
@apply bg-light-purple;
|
||||
@apply dark:bg-dark-purple;
|
||||
@apply text-light-purple;
|
||||
@apply dark:text-dark-purple;
|
||||
}
|
||||
|
||||
</style>
|
||||
</script>
|
||||
@@ -1,7 +1,12 @@
|
||||
<template>
|
||||
<span class="before:content-['#'] cursor-default bg-accent-blue bg-opacity-10 hover:bg-opacity-20 text-accent-blue text-sm px-1 ms-1 pb-0.5 rounded-full rounded-se-none border border-accent-blue border-opacity-30">
|
||||
<slot></slot>
|
||||
</span>
|
||||
<HoverCard nuxt-client class="min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] w-full overflow-auto z-[45]">
|
||||
<template #content>
|
||||
<Markdown class="!px-6" path="tags" :filter="tag" popover />
|
||||
</template>
|
||||
<span class="before:content-['#'] cursor-default bg-accent-blue bg-opacity-10 hover:bg-opacity-20 text-accent-blue text-sm px-1 ms-1 pb-0.5 rounded-full rounded-se-none border border-accent-blue border-opacity-30">
|
||||
<slot></slot>
|
||||
</span>
|
||||
</HoverCard>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@@ -13,4 +18,13 @@
|
||||
{
|
||||
@apply bg-accent-blue bg-opacity-10 text-accent-blue text-sm pb-0.5 pe-1 rounded-r-[12px] !rounded-se-none border border-l-0 border-accent-blue border-opacity-30;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { tag } = defineProps<{
|
||||
tag: string
|
||||
}>();
|
||||
|
||||
const { content } = useContent();
|
||||
const overview = computed(() => content.value.find(e => e.path === "tags"));
|
||||
</script>
|
||||
Reference in New Issue
Block a user