Menu
Public repository / 0d7f5f902b20084326479f36d4ccf2b55ee890c0
AkurAI-Mail / src/lib/webmail/RichTextEditor.svelte
Latest change 22202d1bde69be4ace8a299dffd91f8ff24378df - Recover deployed webmail frontend (frontend parity with EC2 a6afffc) by Ólafur Búi Ólafsson
<script lang="ts">
import { Bold, Italic, Link2, List, ListOrdered, Quote } from "lucide-svelte";
let { html = $bindable(""), text = $bindable(""), placeholder = "", resetKey = 0 }: { html?: string; text?: string; placeholder?: string; resetKey?: number } = $props();
let editor: HTMLDivElement;
let loadedKey = -1;
// Load `html` into the editor only when the parent bumps resetKey (new compose, reply, forward),
// so typing does not reset the caret.
$effect(() => {
if (editor && resetKey !== loadedKey) {
loadedKey = resetKey;
editor.innerHTML = html;
text = editor.innerText;
}
});
function sync() {
html = editor.innerHTML;
text = editor.innerText;
}
function exec(command: string, value?: string) {
editor.focus();
document.execCommand(command, false, value);
sync();
}
function insertLink() {
const url = window.prompt("Link URL");
if (url) exec("createLink", url);
}
</script>
<div class="rte-toolbar" role="toolbar" aria-label="Formatting">
<button type="button" aria-label="Bold" title="Bold" onclick={() => exec("bold")}><Bold size={16} /></button>
<button type="button" aria-label="Italic" title="Italic" onclick={() => exec("italic")}><Italic size={16} /></button>
<button type="button" aria-label="Bulleted list" title="Bulleted list" onclick={() => exec("insertUnorderedList")}><List size={16} /></button>
<button type="button" aria-label="Numbered list" title="Numbered list" onclick={() => exec("insertOrderedList")}><ListOrdered size={16} /></button>
<button type="button" aria-label="Link" title="Link" onclick={insertLink}><Link2 size={16} /></button>
<button type="button" aria-label="Quote" title="Quote" onclick={() => exec("formatBlock", "blockquote")}><Quote size={16} /></button>
</div>
<div class="rte-body" bind:this={editor} contenteditable="true" role="textbox" aria-multiline="true" aria-label="Message" data-placeholder={placeholder} oninput={sync}></div>
<style>
.rte-toolbar {
border-bottom: 1px solid var(--line-soft);
gap: 2px;
padding: 4px 0 8px;
display: flex;
}
.rte-toolbar button {
block-size: 30px;
inline-size: 30px;
color: var(--ink);
background: 0 0;
border: 1px solid #0000;
border-radius: 6px;
place-items: center;
padding: 0;
display: grid;
}
.rte-toolbar button:hover {
background: var(--surface-soft);
border-color: var(--line);
}
.rte-body {
outline: none;
flex: 1;
min-height: 190px;
padding: 8px 0;
overflow: auto;
}
.rte-body:empty:before {
content: attr(data-placeholder);
color: var(--muted);
opacity: 0.82;
}
.rte-body :global(blockquote) {
border-left: 3px solid var(--line);
color: var(--muted);
margin: 6px 0;
padding-left: 10px;
}
.rte-body :global(ul),
.rte-body :global(ol) {
padding-left: 22px;
}
.rte-body :global(a) {
color: var(--accent);
}
</style>