This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / web / src / lib / components / ui / MarkdownEditor.svelte
3.2 kB 132 lines
1<script lang="ts"> 2 import type { KeyboardEventHandler } from "svelte/elements"; 3 import Eye from "$icon/eye"; 4 import Pencil from "$icon/pencil"; 5 import Button from "./Button.svelte"; 6 import ButtonGroup from "./ButtonGroup.svelte"; 7 import Textarea from "./Textarea.svelte"; 8 import { renderMarkup, type MarkupContext } from "$lib/markup"; 9 10 interface Props { 11 value?: string; 12 markup: MarkupContext; 13 tab?: "write" | "preview"; 14 id?: string; 15 name?: string; 16 rows?: number; 17 placeholder?: string; 18 disabled?: boolean; 19 previewClass?: string; 20 autofocus?: boolean; 21 // drop the textarea/preview background so the editor blends into its surroundings 22 transparent?: boolean; 23 } 24 25 let { 26 value = $bindable(""), 27 markup, 28 tab = $bindable("write"), 29 id, 30 name, 31 rows = 12, 32 placeholder, 33 disabled = false, 34 previewClass = "min-h-40", 35 autofocus = false, 36 transparent = false 37 }: Props = $props(); 38 39 const surface = $derived(transparent ? "bg-transparent" : "bg-background-default"); 40 41 let previewHtml = $state<string | null>(null); 42 let previewing = $state(false); 43 let textareaEl = $state<HTMLTextAreaElement>(); 44 45 // focus on mount (and when returning to the write tab) if requested 46 $effect(() => { 47 if (autofocus) textareaEl?.focus(); 48 }); 49 50 $effect(() => { 51 if (tab !== "preview") return; 52 const source = value; 53 if (!source.trim()) { 54 previewHtml = null; 55 previewing = false; 56 return; 57 } 58 let cancelled = false; 59 previewing = true; 60 renderMarkup(source, markup) 61 .then((html) => { 62 if (!cancelled) previewHtml = html; 63 }) 64 .catch(() => { 65 if (!cancelled) previewHtml = null; 66 }) 67 .finally(() => { 68 if (!cancelled) previewing = false; 69 }); 70 return () => { 71 cancelled = true; 72 }; 73 }); 74 // ctrl/cmd+enter submits the enclosing form, mirroring the old htmx editor 75 const handleKeydown: KeyboardEventHandler<HTMLTextAreaElement> = (e) => { 76 if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { 77 e.preventDefault(); 78 e.currentTarget.form?.requestSubmit(); 79 } 80 }; 81</script> 82 83<div class="flex flex-col gap-1.5"> 84 <ButtonGroup class="self-start"> 85 <Button 86 type="button" 87 size="sm" 88 icon={Pencil} 89 variant={tab === "write" ? "default" : "ghost"} 90 onclick={() => (tab = "write")} 91 > 92 Write 93 </Button> 94 <Button 95 type="button" 96 size="sm" 97 icon={Eye} 98 variant={tab === "preview" ? "default" : "ghost"} 99 onclick={() => (tab = "preview")} 100 > 101 Preview 102 </Button> 103 </ButtonGroup> 104 105 {#if tab === "write"} 106 <Textarea 107 {id} 108 {name} 109 {rows} 110 resizeable 111 {placeholder} 112 bind:value 113 bind:element={textareaEl} 114 {disabled} 115 onkeydown={handleKeydown} 116 class={transparent ? "max-h-none bg-transparent" : "max-h-none"} 117 /> 118 {:else if previewHtml} 119 <div 120 class={`markup rounded border border-border-default ${surface} px-2.5 py-2 ${previewClass}`} 121 > 122 <!-- eslint-disable-next-line svelte/no-at-html-tags -- sanitised in $lib/markup --> 123 {@html previewHtml} 124 </div> 125 {:else} 126 <div 127 class={`rounded border border-border-default ${surface} px-2.5 py-2 text-sm text-foreground-subtle italic ${previewClass}`} 128 > 129 {previewing ? "Rendering…" : "Nothing to preview."} 130 </div> 131 {/if} 132</div>