fix(ContentEditable): change logic to support controlled state
This commit is contained in:
37
src/features/DisplayFont/ui/FontSampler/FontSampler.svelte
Normal file
37
src/features/DisplayFont/ui/FontSampler/FontSampler.svelte
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<!--
|
||||||
|
Component: FontSampler
|
||||||
|
Displays a sample text with a given font in a contenteditable element.
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import { appliedFontsManager } from '$entities/Font';
|
||||||
|
import { ContentEditable } from '$shared/ui';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
fontId: string;
|
||||||
|
text?: string;
|
||||||
|
fontSize?: number;
|
||||||
|
lineHeight?: number;
|
||||||
|
letterSpacing?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
fontId,
|
||||||
|
...restProps
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
// Ensure the font is registered as soon as this sampler appears
|
||||||
|
$effect(() => {
|
||||||
|
appliedFontsManager.registerFonts([fontId]);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="
|
||||||
|
w-full rounded-xl
|
||||||
|
bg-white p-6 border border-slate-200
|
||||||
|
shadow-sm dark:border-slate-800 dark:bg-slate-950
|
||||||
|
"
|
||||||
|
style:font-family={fontId}
|
||||||
|
>
|
||||||
|
<ContentEditable {...restProps} />
|
||||||
|
</div>
|
||||||
@@ -7,7 +7,7 @@ interface Props {
|
|||||||
/**
|
/**
|
||||||
* Visible text
|
* Visible text
|
||||||
*/
|
*/
|
||||||
text?: string;
|
text: string;
|
||||||
/**
|
/**
|
||||||
* Font settings
|
* Font settings
|
||||||
*/
|
*/
|
||||||
@@ -17,19 +17,38 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
text = 'The quick brown fox jumps over the lazy dog',
|
text = $bindable('The quick brown fox jumps over the lazy dog.'),
|
||||||
fontSize = 48,
|
fontSize = 48,
|
||||||
lineHeight = 1.2,
|
lineHeight = 1.2,
|
||||||
letterSpacing = 0,
|
letterSpacing = 0,
|
||||||
}: Props = $props();
|
}: 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>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
bind:this={element}
|
||||||
contenteditable="plaintext-only"
|
contenteditable="plaintext-only"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
role="textbox"
|
role="textbox"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
data-placeholder="Type something to test..."
|
data-placeholder="Type something to test..."
|
||||||
|
oninput={handleInput}
|
||||||
class="
|
class="
|
||||||
w-full min-h-[1.2em] outline-none transition-all duration-200
|
w-full min-h-[1.2em] outline-none transition-all duration-200
|
||||||
empty:before:content-[attr(data-placeholder)] empty:before:text-slate-400
|
empty:before:content-[attr(data-placeholder)] empty:before:text-slate-400
|
||||||
@@ -40,5 +59,4 @@ let {
|
|||||||
style:line-height={lineHeight}
|
style:line-height={lineHeight}
|
||||||
style:letter-spacing="{letterSpacing}em"
|
style:letter-spacing="{letterSpacing}em"
|
||||||
>
|
>
|
||||||
{text}
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user