Reusable MarkdownEditor component using EasyMDE: - Toolbar: bold, italic, headings, quotes, lists, links, images, tables, code blocks, preview, side-by-side, fullscreen - Dark mode support with custom CSS overrides - Value syncs via bind:value for form submission - Applied to both wiki new and edit pages - Replaces the plain textarea + manual preview toggle Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
interface Props {
|
||||
name?: string;
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
let { name = 'content', value = $bindable(''), placeholder = 'Write in Markdown...' }: Props = $props();
|
||||
|
||||
let textareaEl: HTMLTextAreaElement;
|
||||
let editor: any = null;
|
||||
|
||||
onMount(async () => {
|
||||
if (!browser) return;
|
||||
|
||||
const EasyMDE = (await import('easymde')).default;
|
||||
await import('easymde/dist/easymde.min.css');
|
||||
|
||||
editor = new EasyMDE({
|
||||
element: textareaEl,
|
||||
initialValue: value,
|
||||
spellChecker: false,
|
||||
autofocus: false,
|
||||
placeholder,
|
||||
status: false,
|
||||
minHeight: '300px',
|
||||
toolbar: [
|
||||
'bold', 'italic', 'heading', '|',
|
||||
'quote', 'unordered-list', 'ordered-list', '|',
|
||||
'link', 'image', 'table', 'code', '|',
|
||||
'preview', 'side-by-side', 'fullscreen', '|',
|
||||
'guide'
|
||||
],
|
||||
sideBySideFullscreen: false
|
||||
});
|
||||
|
||||
editor.codemirror.on('change', () => {
|
||||
value = editor.value();
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (editor) {
|
||||
editor.toTextArea();
|
||||
editor = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="markdown-editor">
|
||||
<textarea bind:this={textareaEl} id={name}></textarea>
|
||||
<input type="hidden" {name} value={value} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.markdown-editor :global(.EasyMDEContainer) {
|
||||
border: none;
|
||||
}
|
||||
.markdown-editor :global(.EasyMDEContainer .CodeMirror) {
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.375rem;
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.markdown-editor :global(.editor-toolbar) {
|
||||
border: 1px solid #d1d5db;
|
||||
border-bottom: none;
|
||||
border-radius: 0.375rem 0.375rem 0 0;
|
||||
opacity: 1;
|
||||
}
|
||||
.markdown-editor :global(.editor-toolbar button) {
|
||||
color: #6b7280 !important;
|
||||
}
|
||||
.markdown-editor :global(.editor-toolbar button:hover) {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
.markdown-editor :global(.editor-toolbar button.active) {
|
||||
background: #e5e7eb;
|
||||
}
|
||||
/* Dark mode */
|
||||
:global(.dark) .markdown-editor :global(.CodeMirror) {
|
||||
background: #374151;
|
||||
color: #f3f4f6;
|
||||
border-color: #4b5563;
|
||||
}
|
||||
:global(.dark) .markdown-editor :global(.editor-toolbar) {
|
||||
background: #1f2937;
|
||||
border-color: #4b5563;
|
||||
}
|
||||
:global(.dark) .markdown-editor :global(.editor-toolbar button) {
|
||||
color: #9ca3af !important;
|
||||
}
|
||||
:global(.dark) .markdown-editor :global(.editor-toolbar button:hover) {
|
||||
background: #374151;
|
||||
}
|
||||
:global(.dark) .markdown-editor :global(.CodeMirror-cursor) {
|
||||
border-color: #f3f4f6;
|
||||
}
|
||||
:global(.dark) .markdown-editor :global(.editor-preview) {
|
||||
background: #374151;
|
||||
color: #f3f4f6;
|
||||
}
|
||||
:global(.dark) .markdown-editor :global(.editor-preview-side) {
|
||||
background: #374151;
|
||||
color: #f3f4f6;
|
||||
border-color: #4b5563;
|
||||
}
|
||||
</style>
|
||||
@@ -1,14 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { marked } from 'marked';
|
||||
import TagInput from '$lib/components/ui/TagInput.svelte';
|
||||
import MarkdownEditor from '$lib/components/ui/MarkdownEditor.svelte';
|
||||
|
||||
let { data, form } = $props();
|
||||
|
||||
let content = $state(data.page.content);
|
||||
let showPreview = $state(false);
|
||||
|
||||
const renderedContent = $derived(marked(content) as string);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -54,24 +51,8 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="mb-1 flex items-center justify-between">
|
||||
<label for="content" class="text-sm font-medium text-gray-700 dark:text-gray-300">Content * (Markdown)</label>
|
||||
<button type="button" onclick={() => (showPreview = !showPreview)}
|
||||
class="rounded-md px-2 py-1 text-xs text-blue-600 hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-blue-900/20">
|
||||
{showPreview ? 'Editor' : 'Preview'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if showPreview}
|
||||
<div class="prose prose-sm dark:prose-invert min-h-[300px] max-w-none rounded-md border border-gray-300 bg-white p-4 dark:border-gray-600 dark:bg-gray-700">
|
||||
{@html renderedContent}
|
||||
</div>
|
||||
<input type="hidden" name="content" value={content} />
|
||||
{:else}
|
||||
<textarea id="content" name="content" rows="20" required
|
||||
bind:value={content}
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 font-mono text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white"></textarea>
|
||||
{/if}
|
||||
<span class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Content * (Markdown)</span>
|
||||
<MarkdownEditor bind:value={content} />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { marked } from 'marked';
|
||||
import TagInput from '$lib/components/ui/TagInput.svelte';
|
||||
import MarkdownEditor from '$lib/components/ui/MarkdownEditor.svelte';
|
||||
|
||||
let { data, form } = $props();
|
||||
|
||||
let content = $state('');
|
||||
let showPreview = $state(false);
|
||||
|
||||
const renderedContent = $derived(marked(content) as string);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -54,25 +51,8 @@
|
||||
|
||||
<!-- Markdown editor -->
|
||||
<div>
|
||||
<div class="mb-1 flex items-center justify-between">
|
||||
<label for="content" class="text-sm font-medium text-gray-700 dark:text-gray-300">Content * (Markdown)</label>
|
||||
<button type="button" onclick={() => (showPreview = !showPreview)}
|
||||
class="rounded-md px-2 py-1 text-xs text-blue-600 hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-blue-900/20">
|
||||
{showPreview ? 'Editor' : 'Preview'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if showPreview}
|
||||
<div class="prose prose-sm dark:prose-invert min-h-[300px] max-w-none rounded-md border border-gray-300 bg-white p-4 dark:border-gray-600 dark:bg-gray-700">
|
||||
{@html renderedContent}
|
||||
</div>
|
||||
<input type="hidden" name="content" value={content} />
|
||||
{:else}
|
||||
<textarea id="content" name="content" rows="15" required
|
||||
bind:value={content}
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 font-mono text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400"
|
||||
placeholder="# Getting Started Write your guide in Markdown... ## Steps 1. First step 2. Second step **Bold text** and `code` are supported."></textarea>
|
||||
{/if}
|
||||
<span class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Content * (Markdown)</span>
|
||||
<MarkdownEditor bind:value={content} placeholder="Write your guide in Markdown..." />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
|
||||
Reference in New Issue
Block a user