New CM6 live edition components and floating cache and persistance.
This commit is contained in:
parent
b9970ccdf8
commit
ab36eec4de
7
app.vue
7
app.vue
|
|
@ -33,6 +33,13 @@ onBeforeMount(() => {
|
|||
</script>
|
||||
|
||||
<style>
|
||||
iconify-icon
|
||||
{
|
||||
display: inline-block;
|
||||
width: attr(width px, 1rem);
|
||||
height: attr(height px, 1rem);
|
||||
box-sizing: content-box;
|
||||
}
|
||||
.ToastRoot[data-type='error'] {
|
||||
@apply border-light-red;
|
||||
@apply dark:border-dark-red;
|
||||
|
|
|
|||
|
|
@ -61,8 +61,6 @@ import { link } from '#shared/components.util';
|
|||
const open = ref(false);
|
||||
const { loggedIn, user } = useUserSession();
|
||||
|
||||
await fetch(false);
|
||||
|
||||
const route = useRouter().currentRoute;
|
||||
const path = computed(() => route.value.params.path ? decodeURIComponent(unifySlug(route.value.params.path)) : undefined);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,31 +2,43 @@
|
|||
<Head>
|
||||
<Title>d[any] - Modification</Title>
|
||||
</Head>
|
||||
<div class="flex flex-1 flex-col xl:-mx-12 xl:-my-8 lg:-mx-8 lg:-my-6 -mx-6 -my-3 overflow-hidden">
|
||||
<div class="z-30 flex w-full items-center justify-between border-b border-light-35 dark:border-dark-35 px-2">
|
||||
<div class="flex items-center px-2 gap-4">
|
||||
<!-- <CollapsibleTrigger asChild>
|
||||
<Button icon class="!bg-transparent group md:hidden">
|
||||
<Icon class="group-data-[state=open]:hidden" icon="radix-icons:hamburger-menu" />
|
||||
<Icon class="group-data-[state=closed]:hidden" icon="radix-icons:cross-1" />
|
||||
</Button>
|
||||
</CollapsibleTrigger> -->
|
||||
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-opacity-70 m-2 flex items-center gap-4" aria-label="Accueil" :to="{ path: '/', force: true }">
|
||||
<Avatar src="/logo.dark.svg" class="dark:block hidden" />
|
||||
<Avatar src="/logo.light.svg" class="block dark:hidden" />
|
||||
<span class="text-xl max-md:hidden">d[any]</span>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<div class="flex items-center px-2 gap-4">
|
||||
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-light-70 dark:hover:text-dark-70" :to="{ name: 'user-login' }">{{ user!.username }}</NuxtLink>
|
||||
<div class="flex flex-row w-full max-w-full h-full max-h-full xl:-mx-12 xl:-my-8 lg:-mx-8 lg:-my-6 -mx-6 -my-3" style="--sidebar-width: 300px">
|
||||
<div class="bg-light-0 dark:bg-dark-0 w-[var(--sidebar-width)] border-r border-light-30 dark:border-dark-30 flex flex-col gap-2">
|
||||
<NuxtLink class="flex flex-row items-center justify-center group gap-2 my-2" aria-label="Accueil" :to="{ name: 'index', force: true }">
|
||||
<Avatar src="/logo.dark.svg" class="dark:block hidden" />
|
||||
<Avatar src="/logo.light.svg" class="block dark:hidden" />
|
||||
<span class="text-xl font-semibold group-hover:text-light-70 dark:group-hover:text-dark-70">d[any]</span>
|
||||
</NuxtLink>
|
||||
<div class="flex-1 px-2 max-w-full max-h-full overflow-y-auto overflow-x-hidden" ref="tree"></div>
|
||||
<div class="flex flex-col my-4 items-center justify-center gap-1 text-xs text-light-60 dark:text-dark-60">
|
||||
<NuxtLink class="hover:underline" :to="{ name: 'legal' }">Mentions légales</NuxtLink>
|
||||
<NuxtLink class="hover:underline" :to="{ name: 'usage' }">Conditions d'utilisations</NuxtLink>
|
||||
Copyright Peaceultime - 2025
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-1 flex-row relative h-screen overflow-hidden">
|
||||
<div class="bg-light-0 dark:bg-dark-0 z-40 w-screen md:w-[18rem] border-r border-light-30 dark:border-dark-30 flex flex-col justify-between my-2 max-md:data-[state=closed]:hidden">
|
||||
<div class="flex-1 px-2 max-w-full max-h-full overflow-y-auto overflow-x-hidden" ref="tree"></div>
|
||||
<div class="xl:px-12 px-6 pt-4 pb-2 text-center text-xs text-light-60 dark:text-dark-60">
|
||||
<NuxtLink class="hover:underline italic" :to="{ name: 'roadmap' }">Roadmap</NuxtLink> - <NuxtLink class="hover:underline italic" :to="{ name: 'legal' }">Mentions légales</NuxtLink>
|
||||
<p>Copyright Peaceultime - 2025</p>
|
||||
<div class="flex flex-col flex-1 h-full w-[calc(100vw-var(--sidebar-width))]">
|
||||
<div class="flex flex-row border-b border-light-30 dark:border-dark-30 justify-between px-8">
|
||||
<div class="flex flex-row gap-16 items-center">
|
||||
<NavigationMenuRoot class="relative">
|
||||
<NavigationMenuList class="flex items-center gap-8 max-md:hidden">
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuTrigger>
|
||||
<NuxtLink :href="{ name: 'character' }" class="flex flex-row gap-2 items-center border-b-2 border-transparent hover:border-accent-blue py-4 select-none" active-class="!text-accent-blue"><span class="px-3 flex-1 truncate">Personnages</span><Icon icon="radix-icons:caret-down" /></NuxtLink>
|
||||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent class="absolute top-0 w-full sm:w-auto bg-light-0 dark:bg-dark-0 border border-light-30 dark:border-dark-30 py-2 z-20 flex flex-col">
|
||||
<NuxtLink :href="{ name: 'character-list' }" class="hover:bg-light-30 dark:hover:bg-dark-30 px-4 py-2 select-none" active-class="!text-accent-blue"><span class="flex-1 truncate">Personnages publics</span></NuxtLink>
|
||||
<NuxtLink :href="{ name: 'character-id-edit', params: { id: 'new' } }" class="hover:bg-light-30 dark:hover:bg-dark-30 px-4 py-2 select-none" active-class="!text-accent-blue"><span class="flex-1 truncate">Nouveau personnage</span></NuxtLink>
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
</NavigationMenuList>
|
||||
<div class="absolute top-full left-0 flex w-full justify-center">
|
||||
<NavigationMenuViewport class="h-[var(--radix-navigation-menu-viewport-height)] w-full origin-[top_center] flex justify-center overflow-hidden sm:w-[var(--radix-navigation-menu-viewport-width)]" />
|
||||
</div>
|
||||
</NavigationMenuRoot>
|
||||
<NuxtLink :href="{ name: 'character' }" class="flex flex-row gap-2 items-center border-b-2 border-transparent hover:border-accent-blue py-4 select-none" active-class="!text-accent-blue"><span class="px-3 flex-1 truncate">Campagnes</span></NuxtLink>
|
||||
</div>
|
||||
<div class="flex flex-row gap-16 items-center">
|
||||
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-light-70 dark:hover:text-dark-70" :to="{ name: 'user-login' }">{{ user!.username }}</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-1 flex-row max-h-full overflow-hidden" ref="container"></div>
|
||||
|
|
@ -40,6 +52,7 @@ import { button, loading } from '#shared/components.util';
|
|||
import { dom, icon } from '#shared/dom.util';
|
||||
import { modal, tooltip } from '#shared/floating.util';
|
||||
import { Toaster } from '#shared/components.util';
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
|
||||
definePageMeta({
|
||||
rights: ['admin', 'editor'],
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { RouteLocationAsRelativeTyped, RouteLocationRaw, RouteMapGeneric } from "vue-router";
|
||||
import { type NodeProperties, type Class, type NodeChildren, dom, mergeClasses, text, div, icon, type Node } from "./dom.util";
|
||||
import { contextmenu, followermenu, popper, tooltip, type FloatState } from "./floating.util";
|
||||
import { contextmenu, followermenu, minimizeBox, popper, teleport, tooltip, type FloatState } from "./floating.util";
|
||||
import { clamp } from "./general.util";
|
||||
import { Tree } from "./tree";
|
||||
import type { Placement } from "@floating-ui/dom";
|
||||
|
|
@ -526,16 +526,16 @@ export function tabgroup(tabs: Array<{ id: string, title: NodeChildren, content:
|
|||
})
|
||||
return container as HTMLDivElement & { refresh: () => void };
|
||||
}
|
||||
export function floater(container: HTMLElement, content: NodeChildren | (() => NodeChildren), settings?: { href?: RouteLocationRaw, class?: Class, position?: Placement, pinned?: boolean, minimizable?: boolean, cover?: 'width' | 'height' | 'all' | 'none', events?: { show: Array<keyof HTMLElementEventMap>, hide: Array<keyof HTMLElementEventMap>, onshow?: (state: FloatState) => boolean, onhide?: (state: FloatState) => boolean }, title?: string })
|
||||
export function floater(container: HTMLElement, content: NodeChildren | (() => NodeChildren), settings?: { href?: RouteLocationRaw, class?: Class, style?: Record<string, string | undefined | boolean | number> | string, position?: Placement, pinned?: boolean, minimizable?: boolean, cover?: 'width' | 'height' | 'all' | 'none', events?: { show: Array<keyof HTMLElementEventMap>, hide: Array<keyof HTMLElementEventMap>, onshow?: (state: FloatState) => boolean, onhide?: (state: FloatState) => boolean }, title?: string })
|
||||
{
|
||||
let viewport = document.getElementById('mainContainer') ?? undefined;
|
||||
let diffX, diffY;
|
||||
let minimizeBox: DOMRect, minimized = false;
|
||||
let minimizeRect: DOMRect, minimized = false;
|
||||
|
||||
const events: { show: Array<keyof HTMLElementEventMap>, hide: Array<keyof HTMLElementEventMap>, onshow?: (this: HTMLElement, state: FloatState) => boolean, onhide?: (this: HTMLElement, state: FloatState) => boolean } = Object.assign({
|
||||
show: ['mouseenter', 'mousemove', 'focus'],
|
||||
hide: ['mouseleave', 'blur'],
|
||||
}, settings?.events ?? {});
|
||||
} as { show: Array<keyof HTMLElementEventMap>, hide: Array<keyof HTMLElementEventMap> }, settings?.events ?? {});
|
||||
|
||||
if(settings?.pinned)
|
||||
{
|
||||
|
|
@ -631,23 +631,30 @@ export function floater(container: HTMLElement, content: NodeChildren | (() => N
|
|||
floating.content.toggleAttribute('data-minimized', minimized);
|
||||
if(minimized)
|
||||
{
|
||||
minimizeBox = floating.content.getBoundingClientRect();
|
||||
minimizeRect = floating.content.getBoundingClientRect();
|
||||
Object.assign(floating.content.style, {
|
||||
left: `0px`,
|
||||
top: `initial`,
|
||||
bottom: `0px`,
|
||||
width: `150px`,
|
||||
height: `21px`,
|
||||
position: 'initial',
|
||||
});
|
||||
floating.content.style.setProperty('top', null);
|
||||
floating.content.style.setProperty('left', null);
|
||||
floating.content.style.setProperty('bottom', null);
|
||||
floating.content.style.setProperty('right', null);
|
||||
|
||||
minimizeBox.appendChild(floating.content);
|
||||
}
|
||||
else
|
||||
{
|
||||
Object.assign(floating.content.style, {
|
||||
left: `${minimizeBox.left}px`,
|
||||
top: `${minimizeBox.top}px`,
|
||||
width: `${minimizeBox.width}px`,
|
||||
height: `${minimizeBox.height}px`,
|
||||
left: `${minimizeRect.left}px`,
|
||||
top: `${minimizeRect.top}px`,
|
||||
width: `${minimizeRect.width}px`,
|
||||
height: `${minimizeRect.height}px`,
|
||||
});
|
||||
floating.content.style.setProperty('position', null);
|
||||
|
||||
teleport.appendChild(floating.content);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -657,10 +664,11 @@ export function floater(container: HTMLElement, content: NodeChildren | (() => N
|
|||
offset: 12,
|
||||
cover: settings?.cover,
|
||||
placement: settings?.position,
|
||||
style: settings?.style,
|
||||
class: 'bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 group-data-[pinned]:bg-light-15 dark:group-data-[pinned]:bg-dark-15 group-data-[pinned]:border-light-50 dark:group-data-[pinned]:border-dark-50 text-light-100 dark:text-dark-100 z-[45] relative group-data-[pinned]:h-full',
|
||||
content: () => [
|
||||
settings?.pinned !== undefined ? div('hidden group-data-[pinned]:flex flex-row items-center border-b border-light-35 dark:border-dark-35', [
|
||||
dom('span', { class: 'flex-1 w-full h-full cursor-move group-data-[minimized]:cursor-default text-xs px-2', listeners: { mousedown: dragstart }, text: (settings?.title?.substring(0, 1)?.toUpperCase() ?? '') + (settings?.title?.substring(1)?.toLowerCase() ?? '') }),
|
||||
dom('span', { class: 'flex-1 w-full h-5 cursor-move group-data-[minimized]:cursor-default text-xs px-2', listeners: { mousedown: dragstart }, text: (settings?.title?.substring(0, 1)?.toUpperCase() ?? '') + (settings?.title?.substring(1)?.toLowerCase() ?? '') }),
|
||||
settings?.title ? tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { click: minimize } }, [icon('radix-icons:minus', { width: 12, height: 12, class: 'p-1' })]), text('Réduire'), 'top') : undefined,
|
||||
settings?.href ? tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { mousedown: (e) => { ((e.ctrlKey || e.button === 1) ? window.open : useRouter().push)(useRouter().resolve(settings.href!).href); floating.hide(); } } }, [icon('radix-icons:external-link', { width: 12, height: 12, class: 'p-1' })]), 'Ouvrir', 'top') : undefined,
|
||||
tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { mousedown: (e) => {
|
||||
|
|
@ -669,10 +677,10 @@ export function floater(container: HTMLElement, content: NodeChildren | (() => N
|
|||
|
||||
floating.content.toggleAttribute('data-minimized', false);
|
||||
minimized && Object.assign(floating.content.style, {
|
||||
left: `${minimizeBox.left}px`,
|
||||
top: `${minimizeBox.top}px`,
|
||||
width: `${minimizeBox.width}px`,
|
||||
height: `${minimizeBox.height}px`,
|
||||
left: `${minimizeRect.left}px`,
|
||||
top: `${minimizeRect.top}px`,
|
||||
width: `${minimizeRect.width}px`,
|
||||
height: `${minimizeRect.height}px`,
|
||||
});
|
||||
minimized = false;
|
||||
} } }, [icon('radix-icons:cross-1', { width: 12, height: 12, class: 'p-1' })]), 'Fermer', 'top') ]) : undefined,
|
||||
|
|
|
|||
|
|
@ -1,14 +1,18 @@
|
|||
import { crosshairCursor, Decoration, dropCursor, EditorView, keymap, ViewPlugin, ViewUpdate, type DecorationSet } from '@codemirror/view';
|
||||
import { Annotation, EditorState, SelectionRange, type Range } from '@codemirror/state';
|
||||
import { crosshairCursor, Decoration, dropCursor, EditorView, keymap, ViewPlugin, ViewUpdate, WidgetType, type DecorationSet } from '@codemirror/view';
|
||||
import { Annotation, EditorState, SelectionRange, StateField, type Range } from '@codemirror/state';
|
||||
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
|
||||
import { bracketMatching, foldKeymap, HighlightStyle, indentOnInput, syntaxHighlighting, syntaxTree } from '@codemirror/language';
|
||||
import { bracketMatching, 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 { IterMode, Tree, type SyntaxNodeRef } from '@lezer/common';
|
||||
import { tags } from '@lezer/highlight';
|
||||
import { dom } from './dom.util';
|
||||
import { dom } from '#shared/dom.util';
|
||||
import { callout as calloutExtension } from '#shared/grammar/callout.extension';
|
||||
import { wikilink as wikilinkExtension } from '#shared/grammar/wikilink.extension';
|
||||
import { renderMarkdown } from '#shared/markdown.util';
|
||||
import prose, { a, blockquote, tag, h1, h2, h3, h4, h5, hr, li, small, table, td, th, callout } from "#shared/proses";
|
||||
import { tagTag, tag as tagExtension } from './grammar/tag.extension';
|
||||
|
||||
const External = Annotation.define<boolean>();
|
||||
const Hidden = Decoration.mark({ class: 'hidden' });
|
||||
|
|
@ -28,15 +32,88 @@ const highlight = HighlightStyle.define([
|
|||
{ 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.meta, class: 'text-light-60 dark:text-dark-60' },
|
||||
{ tag: tags.link, class: 'text-accent-blue hover:underline' },
|
||||
{ tag: tags.special(tags.link), class: 'text-accent-blue font-semibold' },
|
||||
{ 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: tags.keyword, class: "text-accent-blue" },
|
||||
{ tag: tags.monospace, class: "border border-light-35 dark:border-dark-35 px-2 py-px rounded-sm bg-light-20 dark:bg-dark-20" },
|
||||
{ tag: tagTag, class: "cursor-default bg-accent-blue bg-opacity-10 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 CalloutWidget extends WidgetType
|
||||
{
|
||||
from: number;
|
||||
to: number;
|
||||
title: string;
|
||||
type: string;
|
||||
foldable?: boolean;
|
||||
content: string;
|
||||
|
||||
contentMD: HTMLElement;
|
||||
|
||||
static create(node: SyntaxNodeRef, state: EditorState): CalloutWidget | undefined
|
||||
{
|
||||
let type = '';
|
||||
let title = '';
|
||||
const content: string[] = [];
|
||||
|
||||
let cursor = node.node.cursor();
|
||||
if (!cursor.firstChild()) return undefined;
|
||||
|
||||
do
|
||||
{
|
||||
if (cursor.name === 'CalloutMarker')
|
||||
{
|
||||
const _cursor = cursor.node.cursor();
|
||||
_cursor.lastChild();
|
||||
type = state.doc.sliceString(_cursor.from, _cursor.to).toLowerCase();
|
||||
}
|
||||
else if (cursor.name === 'CalloutTitle')
|
||||
{
|
||||
title = state.doc.sliceString(cursor.from, cursor.to).trim();
|
||||
}
|
||||
else if (cursor.name === 'CalloutLine')
|
||||
{
|
||||
const _cursor = cursor.node.cursor();
|
||||
_cursor.lastChild();
|
||||
content.push(state.doc.sliceString(_cursor.from, _cursor.to));
|
||||
}
|
||||
}
|
||||
while (cursor.nextSibling());
|
||||
|
||||
return new CalloutWidget(node.from, node.to, title || (type.substring(0, 1).toUpperCase() + type.substring(1).toLowerCase()), type, content.join('\n'));
|
||||
}
|
||||
constructor(from: number, to: number, title: string, type: string, content: string, foldable?: boolean)
|
||||
{
|
||||
super();
|
||||
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
|
||||
this.title = title;
|
||||
this.type = type;
|
||||
this.content = content;
|
||||
this.foldable = foldable;
|
||||
|
||||
this.contentMD = renderMarkdown(useMarkdown().parseSync(content), { a, blockquote, tag, callout: callout, h1, h2, h3, h4, h5, hr, li, small, table, td, th });
|
||||
}
|
||||
override eq(other: CalloutWidget)
|
||||
{
|
||||
return this.from === other.from && this.to === other.to;
|
||||
}
|
||||
toDOM(view: EditorView)
|
||||
{
|
||||
return dom('div', { class: 'flex cm-line', listeners: { click: e => view.dispatch({ selection: { anchor: this.from, head: this.to } }) } }, [prose('blockquote', callout, [ this.contentMD ], { title: this.title, type: this.type, fold: this.foldable, class: '!m-px ' }) as HTMLElement | undefined]);
|
||||
}
|
||||
override ignoreEvent(event: Event)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
class Decorator
|
||||
{
|
||||
static hiddenNodes: string[] = [
|
||||
|
|
@ -46,11 +123,15 @@ class Decorator
|
|||
'CodeMark',
|
||||
'CodeInfo',
|
||||
'URL',
|
||||
'CalloutMark',
|
||||
'WikilinkMeta',
|
||||
'WikilinkHref',
|
||||
'TagMeta'
|
||||
]
|
||||
decorations: DecorationSet;
|
||||
constructor(view: EditorView)
|
||||
{
|
||||
this.decorations = Decoration.set(this.iterate(syntaxTree(view.state), view.visibleRanges, []), true);
|
||||
this.decorations = Decoration.set(this.iterate(syntaxTree(view.state), view.visibleRanges, [], view.state), true);
|
||||
}
|
||||
update(update: ViewUpdate)
|
||||
{
|
||||
|
|
@ -59,14 +140,14 @@ class Decorator
|
|||
|
||||
this.decorations = this.decorations.update({
|
||||
filter: (f, t, v) => false,
|
||||
add: this.iterate(syntaxTree(update.state), update.view.visibleRanges, update.state.selection.ranges),
|
||||
add: this.iterate(syntaxTree(update.state), update.view.visibleRanges, update.state.selection.ranges, update.state),
|
||||
sort: true,
|
||||
});
|
||||
}
|
||||
iterate(tree: Tree, visible: readonly {
|
||||
from: number;
|
||||
to: number;
|
||||
}[], selection: readonly SelectionRange[]): Range<Decoration>[]
|
||||
}[], selection: readonly SelectionRange[], state: EditorState): Range<Decoration>[]
|
||||
{
|
||||
const decorations: Range<Decoration>[] = [];
|
||||
|
||||
|
|
@ -74,7 +155,7 @@ class Decorator
|
|||
tree.iterate({
|
||||
from, to, mode: IterMode.IgnoreMounts,
|
||||
enter: node => {
|
||||
if(node.node.parent && selection.some(e => intersects(e, node.node.parent!)))
|
||||
if(node.node.parent && node.node.parent.name !== 'Document' && selection.some(e => intersects(e, node.node.parent!)))
|
||||
return true;
|
||||
|
||||
else if(node.name === 'HeaderMark')
|
||||
|
|
@ -97,20 +178,56 @@ class Decorator
|
|||
return decorations;
|
||||
}
|
||||
}
|
||||
function blockIterate(tree: Tree, state: EditorState): Range<Decoration>[]
|
||||
{
|
||||
const decorations: Range<Decoration>[] = [];
|
||||
const selection = state.selection.ranges;
|
||||
|
||||
tree.iterate({
|
||||
mode: IterMode.IgnoreMounts,
|
||||
enter: node => {
|
||||
if(selection.some(e => intersects(e, node)))
|
||||
return true;
|
||||
|
||||
else if(node.name === 'CalloutBlock')
|
||||
return decorations.push(Decoration.replace({ widget: CalloutWidget.create(node, state), block: true, }).range(node.from, node.to)), false;
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
return decorations;
|
||||
}
|
||||
const BlockDecorator = StateField.define<DecorationSet>({
|
||||
create(state)
|
||||
{
|
||||
return Decoration.set(blockIterate(syntaxTree(state), state), true);
|
||||
},
|
||||
update(decorations, transaction)
|
||||
{
|
||||
if(transaction.docChanged || transaction.selection)
|
||||
return Decoration.set(blockIterate(syntaxTree(transaction.state), transaction.state), true);
|
||||
return decorations.map(transaction.changes);
|
||||
},
|
||||
provide: f => EditorView.decorations.from(f),
|
||||
})
|
||||
|
||||
export class MarkdownEditor
|
||||
{
|
||||
private static _singleton: MarkdownEditor;
|
||||
|
||||
private view: EditorView;
|
||||
private viewer: 'read' | 'live' | 'edit' = 'live';
|
||||
onChange?: (content: string) => void;
|
||||
constructor()
|
||||
{
|
||||
this.view = new EditorView({
|
||||
extensions: [
|
||||
markdown({
|
||||
base: markdownLanguage
|
||||
base: markdownLanguage,
|
||||
extensions: [ calloutExtension, wikilinkExtension, tagExtension ]
|
||||
}),
|
||||
BlockDecorator,
|
||||
history(),
|
||||
search(),
|
||||
dropCursor(),
|
||||
|
|
@ -126,9 +243,7 @@ export class MarkdownEditor
|
|||
...defaultKeymap,
|
||||
...searchKeymap,
|
||||
...historyKeymap,
|
||||
...foldKeymap,
|
||||
...completionKeymap,
|
||||
...lintKeymap
|
||||
...completionKeymap
|
||||
]),
|
||||
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
|
||||
if (viewUpdate.docChanged && !viewUpdate.transactions.some(tr => tr.annotation(External)))
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export interface FloatingProperties
|
|||
style?: Record<string, string | undefined | boolean | number> | string;
|
||||
viewport?: HTMLElement;
|
||||
cover?: 'width' | 'height' | 'all' | 'none';
|
||||
persistant?: boolean;
|
||||
}
|
||||
export interface FollowerProperties extends FloatingProperties
|
||||
{
|
||||
|
|
@ -36,17 +37,35 @@ export interface ModalProperties
|
|||
closeWhenOutside?: boolean;
|
||||
onClose?: () => boolean | void;
|
||||
}
|
||||
type ModalInternals = {
|
||||
container: HTMLElement;
|
||||
content: HTMLElement;
|
||||
stop: Function;
|
||||
start: Function;
|
||||
show: Function;
|
||||
hide: Function;
|
||||
persistant: boolean;
|
||||
};
|
||||
|
||||
let teleport: HTMLDivElement;
|
||||
export let teleport: HTMLDivElement, minimizeBox: HTMLDivElement, cache: ModalInternals[] = [];
|
||||
export function init()
|
||||
{
|
||||
teleport = dom('div', { attributes: { id: 'popper-container' }, class: 'absolute top-0 left-0 z-40' });
|
||||
minimizeBox = dom('div', { attributes: { id: 'minimize-container' }, class: 'absolute bottom-0 left-0 flex flex-row px-4 gap-4 z-40 h-[21px]' });
|
||||
cache = [];
|
||||
document.body.appendChild(teleport);
|
||||
document.body.appendChild(minimizeBox);
|
||||
|
||||
useRouter().afterEach(clear);
|
||||
}
|
||||
function clear()
|
||||
{
|
||||
cache = cache.filter(e => !(!e.persistant && e.content.remove()));
|
||||
}
|
||||
|
||||
export function popper(container: HTMLElement, properties?: PopperProperties)
|
||||
{
|
||||
let state: FloatState = 'hidden', manualStop = false, timeout: Timer;
|
||||
let state: FloatState = 'hidden', timeout: Timer;
|
||||
const arrow = svg('svg', { class: ' group-data-[pinned]:hidden absolute fill-light-35 dark:fill-dark-35', attributes: { width: "12", height: "8", viewBox: "0 0 20 10" } }, [svg('polygon', { attributes: { points: "0,0 20,0 10,10" } })]);
|
||||
const content = dom('div', { class: properties?.class, style: properties?.style });
|
||||
const floater = dom('div', { class: 'fixed hidden group', attributes: { 'data-state': 'closed' } }, [ content, properties?.arrow ? arrow : undefined ]);
|
||||
|
|
@ -201,7 +220,7 @@ export function popper(container: HTMLElement, properties?: PopperProperties)
|
|||
link(container);
|
||||
link(floater);
|
||||
|
||||
return { container, content: floater, stop, start, show: () => {
|
||||
const result = { container, content: floater, stop, start, show: () => {
|
||||
if(typeof properties?.content === 'function')
|
||||
properties.content = properties.content();
|
||||
|
||||
|
|
@ -228,11 +247,13 @@ export function popper(container: HTMLElement, properties?: PopperProperties)
|
|||
floater.setAttribute('data-state', 'closed');
|
||||
floater.classList.toggle('hidden', true);
|
||||
|
||||
manualStop = false;
|
||||
floater.toggleAttribute('data-pinned', false);
|
||||
|
||||
state = 'hidden';
|
||||
} };
|
||||
|
||||
cache.push({ ...result, persistant: properties?.persistant ?? false });
|
||||
return result;
|
||||
}
|
||||
export function followermenu(target: FloatingUI.ReferenceElement, content: NodeChildren, properties?: FollowerProperties)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
import type { MarkdownConfig } from '@lezer/markdown';
|
||||
import { styleTags, tags } from '@lezer/highlight';
|
||||
|
||||
export const callout: MarkdownConfig = {
|
||||
defineNodes: [
|
||||
'CalloutBlock',
|
||||
'CalloutMarker',
|
||||
'CalloutMark',
|
||||
'CalloutType',
|
||||
'CalloutTitle',
|
||||
'CalloutLine',
|
||||
'CalloutContent',
|
||||
],
|
||||
parseBlock: [{
|
||||
name: 'Callout',
|
||||
before: 'Blockquote',
|
||||
parse(cx, line) {
|
||||
const match = /^>\s*\[!(\w+)\](?:\s+(.*))?/.exec(line.text);
|
||||
if (!match || !match[1]) return false; //No match
|
||||
|
||||
const start = cx.lineStart, children = [];
|
||||
|
||||
const quoteEnd = start + line.text.indexOf('[!');
|
||||
const typeStart = quoteEnd + 2;
|
||||
const typeEnd = typeStart + match[1].length;
|
||||
const bracketEnd = typeEnd + 1;
|
||||
|
||||
children.push(cx.elt('CalloutMarker', start, bracketEnd, [ cx.elt('CalloutMark', start, quoteEnd), cx.elt('CalloutType', typeStart, typeEnd) ]));
|
||||
|
||||
if(match[2]) children.push(cx.elt('CalloutTitle', bracketEnd + 1, start + line.text.length));
|
||||
|
||||
while (cx.nextLine() && line.text.startsWith('>'))
|
||||
{
|
||||
const pos = line.text.substring(1).search(/\S/) + 1;
|
||||
children.push(cx.elt('CalloutLine', cx.lineStart, cx.lineStart + line.text.length, [
|
||||
cx.elt('CalloutMark', cx.lineStart, cx.lineStart + pos),
|
||||
cx.elt('CalloutContent', cx.lineStart + pos, cx.lineStart + line.text.length),
|
||||
]))
|
||||
}
|
||||
|
||||
cx.addElement(cx.elt('CalloutBlock', start, cx.lineStart - 1, children));
|
||||
|
||||
return true;
|
||||
}
|
||||
}],
|
||||
props: [
|
||||
styleTags({
|
||||
'CalloutBlock': tags.special(tags.quote),
|
||||
'CalloutMarker': tags.meta,
|
||||
'CalloutMark': tags.meta,
|
||||
'CalloutType': tags.keyword,
|
||||
'CalloutTitle': tags.heading,
|
||||
'CalloutLine': tags.content,
|
||||
'CalloutContent': tags.content,
|
||||
})
|
||||
]
|
||||
};
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import type { MarkdownConfig } from '@lezer/markdown';
|
||||
import { styleTags, Tag, tags } from '@lezer/highlight';
|
||||
|
||||
export const tagTag = Tag.define('tag');
|
||||
export const tag: MarkdownConfig = {
|
||||
defineNodes: [
|
||||
'Tag',
|
||||
'TagMeta',
|
||||
],
|
||||
parseInline: [{
|
||||
name: 'Tag',
|
||||
parse(cx, next, pos)
|
||||
{
|
||||
//35 == '#'
|
||||
if (cx.slice(pos, pos + 1).charCodeAt(0) !== 35 || String.fromCharCode(next).trim() === '') return -1;
|
||||
|
||||
const end = cx.slice(pos, cx.end).search(/\s/);
|
||||
return cx.addElement(cx.elt('Tag', pos, end === -1 ? cx.end : end, [ cx.elt('TagMeta', pos, pos + 1) ]));
|
||||
},
|
||||
}],
|
||||
props: [
|
||||
styleTags({
|
||||
'Tag': tagTag,
|
||||
'TagMeta': tags.meta,
|
||||
})
|
||||
]
|
||||
};
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import type { Element, MarkdownConfig } from '@lezer/markdown';
|
||||
import { styleTags, tags } from '@lezer/highlight';
|
||||
|
||||
export const wikilink: MarkdownConfig = {
|
||||
defineNodes: [
|
||||
'Wikilink',
|
||||
'WikilinkMeta',
|
||||
'WikilinkHref',
|
||||
'WikilinkTitle',
|
||||
],
|
||||
parseInline: [{
|
||||
name: 'Wikilink',
|
||||
before: 'Link',
|
||||
parse(cx, next, pos)
|
||||
{
|
||||
// 91 == '['
|
||||
if (next !== 91 || cx.slice(pos, pos + 1).charCodeAt(0) !== 91) return -1;
|
||||
|
||||
const match = /!?\[\[([^\[\]\|\#]+)?(#+[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/.exec(cx.slice(pos, cx.end));
|
||||
if(!match) return -1;
|
||||
|
||||
const start = pos, children: Element[] = [], end = start + match[0].length;
|
||||
|
||||
children.push(cx.elt('WikilinkMeta', start, start + 2));
|
||||
|
||||
if(match[1] && !match[2] && !match[3]) //Link only
|
||||
{
|
||||
children.push(cx.elt('WikilinkTitle', start + 2, end - 2));
|
||||
}
|
||||
else if(!match[1] && match[2] && match[3]) //Hash and title
|
||||
{
|
||||
children.push(cx.elt('WikilinkHref', start + 2, start + 2 + match[2].length));
|
||||
children.push(cx.elt('WikilinkMeta', start + 2 + match[2].length, start + 2 + match[2].length + 1));
|
||||
children.push(cx.elt('WikilinkTitle', start + 2 + match[2].length + 1, start + 2 + match[2].length + match[3].length));
|
||||
}
|
||||
else if(!match[1] && !match[2] && match[3]) //Hash only
|
||||
{
|
||||
children.push(cx.elt('WikilinkTitle', start + 2, end - 2));
|
||||
}
|
||||
else if(match[1] && match[2] && !match[3]) //Link and hash
|
||||
{
|
||||
children.push(cx.elt('WikilinkHref', start + 2, start + 2 + match[1].length));
|
||||
children.push(cx.elt('WikilinkTitle', start + 2 + match[1].length, start + 2 + match[1].length + match[2].length));
|
||||
}
|
||||
else if(match[1] && !match[2] && match[3]) //Link and title
|
||||
{
|
||||
children.push(cx.elt('WikilinkHref', start + 2, start + 2 + match[1].length));
|
||||
children.push(cx.elt('WikilinkMeta', start + 2 + match[1].length, start + 2 + match[1].length + 1));
|
||||
children.push(cx.elt('WikilinkTitle', start + 2 + match[1].length + 1, start + 2 + match[1].length + match[3].length));
|
||||
}
|
||||
else if(match[1] && match[2] && match[3]) //Link, hash and title
|
||||
{
|
||||
//console.log(cx.slice(pos, end), '/', cx.slice(start + 2, start + 2 + match[1].length + match[2].length), '/', cx.slice(start + 2 + match[1].length + match[2].length, start + 2 + match[1].length + match[2].length + 1), cx.slice(start + 2 + match[1].length + match[2].length + 1, start + 2 + match[1].length + match[2].length + match[3].length))
|
||||
children.push(cx.elt('WikilinkHref', start + 2, start + 2 + match[1].length + match[2].length));
|
||||
children.push(cx.elt('WikilinkMeta', start + 2 + match[1].length + match[2].length, start + 2 + match[1].length + match[2].length + 1));
|
||||
children.push(cx.elt('WikilinkTitle', start + 2 + match[1].length + match[2].length + 1, start + 2 + match[1].length + match[2].length + match[3].length));
|
||||
}
|
||||
|
||||
children.push(cx.elt('WikilinkMeta', end - 2, end));
|
||||
|
||||
return cx.addElement(cx.elt('Wikilink', start, end, children));
|
||||
},
|
||||
}],
|
||||
props: [
|
||||
styleTags({
|
||||
'Wikilink': tags.special(tags.content),
|
||||
'WikilinkMeta': tags.meta,
|
||||
'WikilinkHref': tags.link,
|
||||
'WikilinkTitle': tags.special(tags.link),
|
||||
})
|
||||
]
|
||||
};
|
||||
|
|
@ -64,7 +64,7 @@ export function markdownReference(content: string, filter?: string, properties?:
|
|||
}
|
||||
}
|
||||
|
||||
const el = renderMarkdown(data, { a, blockquote, tag, callout, h1, h2, h3, h4, h5, hr, li, small, table, td, th, ...properties?.tags });
|
||||
const el = renderMarkdown(data, Object.assign({}, { a, blockquote, tag, callout, h1, h2, h3, h4, h5, hr, li, small, table, td, th }, properties?.tags));
|
||||
|
||||
if(properties) styling(el, properties);
|
||||
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ export const callout: Prose = {
|
|||
} = properties;
|
||||
|
||||
let open = fold;
|
||||
const container = dom('div', { class: 'callout group overflow-hidden my-4 p-3 ps-4 bg-blend-lighten !bg-opacity-25 border-l-4 inline-block pe-8 bg-light-blue dark:bg-dark-blue', attributes: { 'data-state': fold !== false ? 'closed' : 'open', 'data-type': type } }, [
|
||||
const container = dom('div', { class: ['callout group overflow-hidden my-4 p-3 ps-4 bg-blend-lighten !bg-opacity-25 border-l-4 inline-block pe-8 bg-light-blue dark:bg-dark-blue', properties?.class], attributes: { 'data-state': fold !== false ? 'closed' : 'open', 'data-type': type } }, [
|
||||
dom('div', { class: [{'cursor-pointer': fold !== undefined}, 'flex flex-row items-center justify-start ps-2'], listeners: { click: e => {
|
||||
container.setAttribute('data-state', open ? 'open' : 'closed');
|
||||
open = !open;
|
||||
|
|
|
|||
Loading…
Reference in New Issue