Legger til en TipTap-basert rik tekst-editor i mottaksflaten som sender create_node-intensjoner til maskinrommet ved submit. - TipTap med StarterKit (tekst, markdown), Link-extension og Placeholder - NodeEditor.svelte: tittel + innhold, Ctrl+Enter for submit - API-klient (lib/api.ts) som kaller maskinrommet via /api proxy - Authentik access_token eksponert i session for API-kall - Vite proxy rewrite fikset (/api → maskinrommet root) - HTML-innhold lagres i metadata.document, ren tekst i content Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
124 lines
3.2 KiB
Svelte
124 lines
3.2 KiB
Svelte
<script lang="ts">
|
|
import { onMount, onDestroy } from 'svelte';
|
|
import { Editor } from '@tiptap/core';
|
|
import StarterKit from '@tiptap/starter-kit';
|
|
import Link from '@tiptap/extension-link';
|
|
import Placeholder from '@tiptap/extension-placeholder';
|
|
|
|
interface Props {
|
|
onsubmit: (data: { title: string; content: string; html: string }) => Promise<void>;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
let { onsubmit, disabled = false }: Props = $props();
|
|
|
|
let editorElement: HTMLDivElement | undefined = $state();
|
|
let editor: Editor | undefined = $state();
|
|
let title = $state('');
|
|
let submitting = $state(false);
|
|
let error = $state('');
|
|
|
|
onMount(() => {
|
|
editor = new Editor({
|
|
element: editorElement!,
|
|
extensions: [
|
|
StarterKit.configure({
|
|
heading: { levels: [2, 3] },
|
|
codeBlock: false,
|
|
horizontalRule: false
|
|
}),
|
|
Link.configure({
|
|
openOnClick: false,
|
|
HTMLAttributes: { class: 'text-blue-600 underline' }
|
|
}),
|
|
Placeholder.configure({
|
|
placeholder: 'Skriv noe…'
|
|
})
|
|
],
|
|
editorProps: {
|
|
attributes: {
|
|
class: 'prose prose-sm max-w-none focus:outline-none min-h-[80px] px-3 py-2'
|
|
}
|
|
},
|
|
onTransaction: () => {
|
|
// Force Svelte reactivity
|
|
editor = editor;
|
|
}
|
|
});
|
|
});
|
|
|
|
onDestroy(() => {
|
|
editor?.destroy();
|
|
});
|
|
|
|
const isEmpty = $derived(!title.trim() && (!editor || editor.isEmpty));
|
|
|
|
async function handleSubmit() {
|
|
if (isEmpty || submitting || disabled) return;
|
|
|
|
submitting = true;
|
|
error = '';
|
|
|
|
try {
|
|
const content = editor?.getText() ?? '';
|
|
const html = editor?.getHTML() ?? '';
|
|
await onsubmit({ title: title.trim(), content, html });
|
|
// Clear editor on success
|
|
title = '';
|
|
editor?.commands.clearContent();
|
|
} catch (e) {
|
|
error = e instanceof Error ? e.message : 'Noe gikk galt';
|
|
} finally {
|
|
submitting = false;
|
|
}
|
|
}
|
|
|
|
function handleKeydown(e: KeyboardEvent) {
|
|
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
|
e.preventDefault();
|
|
handleSubmit();
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
<div class="rounded-lg border border-gray-200 bg-white shadow-sm" onkeydown={handleKeydown}>
|
|
<input
|
|
type="text"
|
|
bind:value={title}
|
|
placeholder="Tittel (valgfritt)"
|
|
disabled={disabled || submitting}
|
|
class="w-full border-b border-gray-100 bg-transparent px-3 py-2 text-sm font-medium text-gray-900 placeholder:text-gray-400 focus:outline-none"
|
|
/>
|
|
|
|
<div bind:this={editorElement}></div>
|
|
|
|
{#if error}
|
|
<p class="px-3 py-1 text-xs text-red-600">{error}</p>
|
|
{/if}
|
|
|
|
<div class="flex items-center justify-between border-t border-gray-100 px-3 py-2">
|
|
<span class="text-xs text-gray-400">
|
|
{#if editor}
|
|
Markdown · Ctrl+B/I · Ctrl+Enter for å sende
|
|
{/if}
|
|
</span>
|
|
<button
|
|
onclick={handleSubmit}
|
|
disabled={isEmpty || submitting || disabled}
|
|
class="rounded bg-blue-600 px-3 py-1 text-xs font-medium text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-40"
|
|
>
|
|
{submitting ? 'Sender…' : 'Opprett node'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
:global(.tiptap p.is-editor-empty:first-child::before) {
|
|
color: #9ca3af;
|
|
content: attr(data-placeholder);
|
|
float: left;
|
|
height: 0;
|
|
pointer-events: none;
|
|
}
|
|
</style>
|