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.3 kB 129 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, { segmentProps } from "./ButtonGroup.svelte"; 7 import Textarea, { textareaMinHeight } 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 autofocus?: boolean; 20 // drop the textarea/preview background so the editor blends into its surroundings 21 transparent?: boolean; 22 } 23 24 let { 25 value = $bindable(""), 26 markup, 27 tab = $bindable("write"), 28 id, 29 name, 30 rows = 12, 31 placeholder, 32 disabled = false, 33 autofocus = false, 34 transparent = false 35 }: Props = $props(); 36 37 const surface = $derived(transparent ? "bg-transparent" : "bg-background-default"); 38 // the preview pane replaces the field in place, so it has to carry the field's own 39 // height and inset — otherwise the whole editor resizes every time you switch tabs. 40 const pane = `rounded border border-border-default px-2.5 py-2 ${textareaMinHeight}`; 41 42 let previewHtml = $state<string | null>(null); 43 let previewing = $state(false); 44 let textareaEl = $state<HTMLTextAreaElement>(); 45 46 // focus on mount (and when returning to the write tab) if requested 47 $effect(() => { 48 if (autofocus) textareaEl?.focus(); 49 }); 50 51 $effect(() => { 52 if (tab !== "preview") return; 53 const source = value; 54 if (!source.trim()) { 55 previewHtml = null; 56 previewing = false; 57 return; 58 } 59 let cancelled = false; 60 previewing = true; 61 renderMarkup(source, markup) 62 .then((html) => { 63 if (!cancelled) previewHtml = html; 64 }) 65 .catch(() => { 66 if (!cancelled) previewHtml = null; 67 }) 68 .finally(() => { 69 if (!cancelled) previewing = false; 70 }); 71 return () => { 72 cancelled = true; 73 }; 74 }); 75 // ctrl/cmd+enter submits the enclosing form, mirroring the old htmx editor 76 const handleKeydown: KeyboardEventHandler<HTMLTextAreaElement> = (e) => { 77 if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { 78 e.preventDefault(); 79 e.currentTarget.form?.requestSubmit(); 80 } 81 }; 82</script> 83 84<div class="flex flex-col gap-1.5"> 85 <ButtonGroup class="self-start"> 86 <Button 87 type="button" 88 size="sm" 89 icon={Pencil} 90 {...segmentProps(tab === "write")} 91 onclick={() => (tab = "write")} 92 > 93 Write 94 </Button> 95 <Button 96 type="button" 97 size="sm" 98 icon={Eye} 99 {...segmentProps(tab === "preview")} 100 onclick={() => (tab = "preview")} 101 > 102 Preview 103 </Button> 104 </ButtonGroup> 105 106 {#if tab === "write"} 107 <Textarea 108 {id} 109 {name} 110 {rows} 111 resizeable 112 {placeholder} 113 bind:value 114 bind:element={textareaEl} 115 {disabled} 116 onkeydown={handleKeydown} 117 class={transparent ? "bg-transparent" : undefined} 118 /> 119 {:else if previewHtml} 120 <div class={`markup ${pane} ${surface}`}> 121 <!-- eslint-disable-next-line svelte/no-at-html-tags -- sanitised in $lib/markup --> 122 {@html previewHtml} 123 </div> 124 {:else} 125 <div class={`${pane} ${surface} text-sm text-foreground-subtle italic`}> 126 {previewing ? "Rendering…" : "Nothing to preview."} 127 </div> 128 {/if} 129</div>