feat: implement math rendering in HTML component and enhance drag handle visibility logic
This commit is contained in:
@@ -1,8 +1,45 @@
|
||||
<script lang="ts">
|
||||
import { onMount, tick } from 'svelte';
|
||||
import { renderMathInElement } from '@cio/ui/tools';
|
||||
|
||||
let { children, className = '', id = '', disableMaxWidth = false } = $props();
|
||||
let container: HTMLElement | undefined;
|
||||
|
||||
onMount(() => {
|
||||
if (!container) return;
|
||||
|
||||
let renderScheduled = false;
|
||||
|
||||
const renderMath = async () => {
|
||||
if (!container || renderScheduled) return;
|
||||
|
||||
renderScheduled = true;
|
||||
|
||||
try {
|
||||
await tick();
|
||||
renderMathInElement(container);
|
||||
} finally {
|
||||
renderScheduled = false;
|
||||
}
|
||||
};
|
||||
|
||||
void renderMath();
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
void renderMath();
|
||||
});
|
||||
|
||||
observer.observe(container, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
return () => observer.disconnect();
|
||||
});
|
||||
</script>
|
||||
|
||||
<article
|
||||
bind:this={container}
|
||||
style:max-width={disableMaxWidth ? 'unset' : undefined}
|
||||
{id}
|
||||
class="preview prose sm:prose-sm {className} dark:text-white"
|
||||
|
||||
@@ -184,10 +184,9 @@ export function DragHandlePlugin(options: GlobalDragHandleOptions & { pluginKey:
|
||||
|
||||
function hideHandleOnEditorOut(event: MouseEvent) {
|
||||
if (event.target instanceof Element) {
|
||||
// Check if the relatedTarget class is still inside the editor
|
||||
// Keep handle visible while cursor moves into editor descendants or handle children.
|
||||
const relatedTarget = event.relatedTarget as HTMLElement;
|
||||
const isInsideEditor =
|
||||
relatedTarget?.classList.contains('tiptap') || relatedTarget?.classList.contains('drag-handle');
|
||||
const isInsideEditor = !!relatedTarget?.closest('.tiptap, .drag-handle');
|
||||
|
||||
if (isInsideEditor) return;
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
pluginKey="link-bubble-menu"
|
||||
{shouldShow}
|
||||
class={cn(
|
||||
'edra-bubble-menu ui:bg-popover ui:flex ui:h-fit ui:w-fit ui:items-center ui:rounded-lg ui:p-0.5',
|
||||
'edra-bubble-menu ui:bg-popover ui:flex ui:h-fit ui:w-fit ui:items-center ui:rounded-lg ui:p-0.5 ui:shadow-lg',
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
export * from './render-math';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import katex from 'katex';
|
||||
import 'katex/dist/katex.min.css';
|
||||
|
||||
const MATH_NODE_SELECTOR = '[data-type="inline-math"], [data-type="block-math"]';
|
||||
|
||||
export function renderMathInElement(root: ParentNode): void {
|
||||
const mathNodes = root.querySelectorAll<HTMLElement>(MATH_NODE_SELECTOR);
|
||||
|
||||
for (const mathNode of mathNodes) {
|
||||
if (mathNode.dataset.mathRendered === 'true') continue;
|
||||
|
||||
const latex = mathNode.getAttribute('data-latex');
|
||||
const mathType = mathNode.getAttribute('data-type');
|
||||
|
||||
if (!latex || (mathType !== 'inline-math' && mathType !== 'block-math')) continue;
|
||||
|
||||
katex.render(latex, mathNode, {
|
||||
displayMode: mathType === 'block-math',
|
||||
throwOnError: false
|
||||
});
|
||||
|
||||
mathNode.dataset.mathRendered = 'true';
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ export function sanitizeHtml(html: string): string {
|
||||
'img'
|
||||
],
|
||||
// Allow safe attributes
|
||||
ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'class', 'style'],
|
||||
ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'class', 'style', 'data-type', 'data-latex'],
|
||||
// Forbid dangerous attributes
|
||||
FORBID_ATTR: [
|
||||
'onerror',
|
||||
|
||||
Reference in New Issue
Block a user