142 lines
3.7 KiB
Svelte
142 lines
3.7 KiB
Svelte
<script lang="ts">
|
|
import { onMount, onDestroy } from 'svelte';
|
|
import { Editor } from '@tiptap/core';
|
|
import StarterKit from '@tiptap/starter-kit';
|
|
import Image from '@tiptap/extension-image';
|
|
import Link from '@tiptap/extension-link';
|
|
import Placeholder from '@tiptap/extension-placeholder';
|
|
import { Markdown } from 'tiptap-markdown';
|
|
import { TiptapMention } from '$lib/editor/tiptapMention';
|
|
import { storeImage, getImageUrl, resolveImageUrls, objectUrlToKaImg } from '$lib/db/imageStore';
|
|
|
|
interface Props {
|
|
content?: string;
|
|
placeholder?: string;
|
|
minHeight?: string;
|
|
onchange?: (markdown: string) => void;
|
|
onsave?: () => void;
|
|
onsavearchive?: () => void;
|
|
}
|
|
let { content = '', placeholder = '', minHeight = '80px', onchange, onsave, onsavearchive }: Props = $props();
|
|
|
|
let element: HTMLDivElement;
|
|
let editor: Editor | null = null;
|
|
let skipNextUpdate = false;
|
|
let lastExternalContent = $state(content);
|
|
|
|
export function getMarkdown(): string {
|
|
if (!editor) return '';
|
|
const raw = editor.storage.markdown.getMarkdown();
|
|
return objectUrlToKaImg(raw);
|
|
}
|
|
|
|
export function clear() {
|
|
if (!editor) return;
|
|
editor.commands.clearContent(true);
|
|
}
|
|
|
|
function emitChange() {
|
|
if (!editor || skipNextUpdate) {
|
|
skipNextUpdate = false;
|
|
return;
|
|
}
|
|
const md = getMarkdown();
|
|
onchange?.(md);
|
|
}
|
|
|
|
function handlePaste(_view: any, event: ClipboardEvent): boolean {
|
|
const items = event.clipboardData?.items;
|
|
if (!items) return false;
|
|
|
|
for (const item of items) {
|
|
if (item.type.startsWith('image/')) {
|
|
event.preventDefault();
|
|
const blob = item.getAsFile();
|
|
if (blob) {
|
|
storeImage(blob)
|
|
.then((id) => getImageUrl(id))
|
|
.then((url) => {
|
|
if (url && editor) {
|
|
editor.chain().focus().setImage({ src: url }).run();
|
|
}
|
|
});
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
onMount(async () => {
|
|
let initialContent = content;
|
|
if (content.includes('ka-img:')) {
|
|
initialContent = await resolveImageUrls(content);
|
|
}
|
|
|
|
editor = new Editor({
|
|
element,
|
|
extensions: [
|
|
StarterKit,
|
|
Image.configure({ inline: true, allowBase64: false }),
|
|
Link.configure({ autolink: true, openOnClick: false }),
|
|
Placeholder.configure({ placeholder }),
|
|
Markdown.configure({ html: false, transformCopiedText: true, transformPastedText: true }),
|
|
TiptapMention
|
|
],
|
|
content: initialContent,
|
|
editorProps: {
|
|
handlePaste,
|
|
handleKeyDown: (_view, event) => {
|
|
if (event.key === 'Enter' && (event.ctrlKey || event.metaKey) && event.shiftKey) {
|
|
onsavearchive?.();
|
|
return true;
|
|
}
|
|
if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
|
|
onsave?.();
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
attributes: {
|
|
style: `min-height: ${minHeight}`
|
|
}
|
|
},
|
|
onUpdate: () => emitChange(),
|
|
onTransaction: ({ editor: e }) => {
|
|
// Force Svelte reactivity by reassigning
|
|
editor = e;
|
|
}
|
|
});
|
|
|
|
// Bind to DOM — Tiptap needs this for Svelte
|
|
element.querySelector('.ProseMirror')?.setAttribute('data-ready', 'true');
|
|
});
|
|
|
|
// React to external content changes
|
|
$effect(() => {
|
|
if (!editor || content === lastExternalContent) return;
|
|
lastExternalContent = content;
|
|
|
|
const currentMd = getMarkdown();
|
|
if (content === currentMd) return;
|
|
|
|
skipNextUpdate = true;
|
|
if (content.includes('ka-img:')) {
|
|
resolveImageUrls(content).then(resolved => {
|
|
editor?.commands.setContent(resolved);
|
|
});
|
|
} else {
|
|
editor.commands.setContent(content);
|
|
}
|
|
});
|
|
|
|
onDestroy(() => {
|
|
editor?.destroy();
|
|
});
|
|
</script>
|
|
|
|
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
|
<div class="ka-editor-wrapper" onclick={(e) => e.stopPropagation()}>
|
|
<div bind:this={element}></div>
|
|
</div>
|