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 126 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 preview = $state<Promise<string> | null>(null); 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") { 52 preview = null; 53 return; 54 } 55 const source = value; 56 if (!source.trim()) { 57 preview = null; 58 return; 59 } 60 preview = renderMarkup(source, markup).then((html) => html ?? ""); 61 }); 62 // ctrl/cmd+enter submits the enclosing form, mirroring the old htmx editor 63 const handleKeydown: KeyboardEventHandler<HTMLTextAreaElement> = (e) => { 64 if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { 65 e.preventDefault(); 66 e.currentTarget.form?.requestSubmit(); 67 } 68 }; 69</script> 70 71<div class="flex flex-col gap-1.5"> 72 <ButtonGroup class="self-start"> 73 <Button 74 type="button" 75 size="sm" 76 icon={Pencil} 77 {...segmentProps(tab === "write")} 78 onclick={() => (tab = "write")} 79 > 80 Write 81 </Button> 82 <Button 83 type="button" 84 size="sm" 85 icon={Eye} 86 {...segmentProps(tab === "preview")} 87 onclick={() => (tab = "preview")} 88 > 89 Preview 90 </Button> 91 </ButtonGroup> 92 93 {#if tab === "write"} 94 <Textarea 95 {id} 96 {name} 97 {rows} 98 resizeable 99 {placeholder} 100 bind:value 101 bind:element={textareaEl} 102 {disabled} 103 onkeydown={handleKeydown} 104 class={transparent ? "bg-transparent" : undefined} 105 /> 106 {:else} 107 {#await preview} 108 <div class={`${pane} ${surface} text-sm text-foreground-subtle italic`}>Rendering…</div> 109 {:then html} 110 {#if html} 111 <div class={`markup ${pane} ${surface}`}> 112 <!-- eslint-disable-next-line svelte/no-at-html-tags -- sanitised in $lib/markup --> 113 {@html html} 114 </div> 115 {:else} 116 <div class={`${pane} ${surface} text-sm text-foreground-subtle italic`}> 117 Nothing to preview. 118 </div> 119 {/if} 120 {:catch} 121 <div class={`${pane} ${surface} text-sm text-foreground-subtle italic`}> 122 Could not render preview. 123 </div> 124 {/await} 125 {/if} 126</div>