feat: implement math rendering in HTML component and enhance drag handle visibility logic

This commit is contained in:
Rotimi Best
2026-03-12 06:44:56 +01:00
parent c87cc4b868
commit f3690c6746
6 changed files with 66 additions and 5 deletions
@@ -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
View File
@@ -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));
+24
View File
@@ -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';
}
}
+1 -1
View File
@@ -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',