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">
|
<script lang="ts">
|
||||||
|
import { onMount, tick } from 'svelte';
|
||||||
|
import { renderMathInElement } from '@cio/ui/tools';
|
||||||
|
|
||||||
let { children, className = '', id = '', disableMaxWidth = false } = $props();
|
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>
|
</script>
|
||||||
|
|
||||||
<article
|
<article
|
||||||
|
bind:this={container}
|
||||||
style:max-width={disableMaxWidth ? 'unset' : undefined}
|
style:max-width={disableMaxWidth ? 'unset' : undefined}
|
||||||
{id}
|
{id}
|
||||||
class="preview prose sm:prose-sm {className} dark:text-white"
|
class="preview prose sm:prose-sm {className} dark:text-white"
|
||||||
|
|||||||
@@ -184,10 +184,9 @@ export function DragHandlePlugin(options: GlobalDragHandleOptions & { pluginKey:
|
|||||||
|
|
||||||
function hideHandleOnEditorOut(event: MouseEvent) {
|
function hideHandleOnEditorOut(event: MouseEvent) {
|
||||||
if (event.target instanceof Element) {
|
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 relatedTarget = event.relatedTarget as HTMLElement;
|
||||||
const isInsideEditor =
|
const isInsideEditor = !!relatedTarget?.closest('.tiptap, .drag-handle');
|
||||||
relatedTarget?.classList.contains('tiptap') || relatedTarget?.classList.contains('drag-handle');
|
|
||||||
|
|
||||||
if (isInsideEditor) return;
|
if (isInsideEditor) return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,7 +102,7 @@
|
|||||||
pluginKey="link-bubble-menu"
|
pluginKey="link-bubble-menu"
|
||||||
{shouldShow}
|
{shouldShow}
|
||||||
class={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { clsx, type ClassValue } from 'clsx';
|
import { clsx, type ClassValue } from 'clsx';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
export * from './render-math';
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
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'
|
'img'
|
||||||
],
|
],
|
||||||
// Allow safe attributes
|
// 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 dangerous attributes
|
||||||
FORBID_ATTR: [
|
FORBID_ATTR: [
|
||||||
'onerror',
|
'onerror',
|
||||||
|
|||||||
Reference in New Issue
Block a user