69 lines
1.7 KiB
Svelte
69 lines
1.7 KiB
Svelte
<!--
|
|
Component: ContentEditable
|
|
Provides a contenteditable div with custom font and text properties.
|
|
-->
|
|
<script lang="ts">
|
|
interface Props {
|
|
/**
|
|
* Visible text (bindable)
|
|
*/
|
|
text: string;
|
|
/**
|
|
* Font size in pixels
|
|
*/
|
|
fontSize?: number;
|
|
/**
|
|
* Line height
|
|
*/
|
|
lineHeight?: number;
|
|
/**
|
|
* Letter spacing in pixels
|
|
*/
|
|
letterSpacing?: number;
|
|
}
|
|
|
|
let {
|
|
text = $bindable('The quick brown fox jumps over the lazy dog.'),
|
|
fontSize = 48,
|
|
lineHeight = 1.2,
|
|
letterSpacing = 0,
|
|
}: Props = $props();
|
|
|
|
let element: HTMLDivElement | undefined = $state();
|
|
|
|
// Initial Sync: Set the text ONLY ONCE when the element is created.
|
|
// This prevents Svelte from "owning" the innerHTML/innerText.
|
|
$effect(() => {
|
|
if (element && element.innerText !== text) {
|
|
element.innerText = text;
|
|
}
|
|
});
|
|
|
|
// Handle changes: Update the outer state without re-rendering the div.
|
|
function handleInput(e: Event) {
|
|
const target = e.target as HTMLDivElement;
|
|
// Update the bindable prop directly
|
|
text = target.innerText;
|
|
}
|
|
</script>
|
|
|
|
<div
|
|
bind:this={element}
|
|
contenteditable="plaintext-only"
|
|
spellcheck="false"
|
|
role="textbox"
|
|
tabindex="0"
|
|
data-placeholder="Type something to test..."
|
|
oninput={handleInput}
|
|
class="
|
|
w-full min-h-[1.2em] outline-none transition-all duration-200
|
|
empty:before:content-[attr(data-placeholder)] empty:before:text-slate-400
|
|
selection:bg-indigo-100 selection:text-indigo-900
|
|
caret-indigo-500 focus:outline-none
|
|
"
|
|
style:font-size="{fontSize}px"
|
|
style:line-height={lineHeight}
|
|
style:letter-spacing="{letterSpacing}em"
|
|
>
|
|
</div>
|