From 445d1c505faa6f4acb95b511c56409e736b0be48 Mon Sep 17 00:00:00 2001 From: Steffen Becker Date: Fri, 31 Jul 2026 18:59:38 +0200 Subject: [PATCH] feat: editable font field --- src/lib/components/ToolbarExpanded.svelte | 132 ++++++++++++++-------- 1 file changed, 88 insertions(+), 44 deletions(-) diff --git a/src/lib/components/ToolbarExpanded.svelte b/src/lib/components/ToolbarExpanded.svelte index 268d8b4..98f3de6 100644 --- a/src/lib/components/ToolbarExpanded.svelte +++ b/src/lib/components/ToolbarExpanded.svelte @@ -297,6 +297,8 @@ } let fontOpen = $state(false); + let fontInputFocused = $state(false); + let fontInputValue = $state(''); let sizeOpen = $state(false); let lineHeightOpen = $state(false); let fontColorOpen = $state(false); @@ -313,6 +315,21 @@ } }); + // The typed text survives the input's blur while the dropdown is open: clicking an + // option blurs first, and re-expanding the list there would move the click target away. + $effect(() => { + if (!fontInputFocused && !fontOpen) fontInputValue = currentFont; + }); + + // While the user types, the dropdown narrows to the matching fonts. + let fontFilter = $derived( + fontOpen && fontInputValue !== currentFont ? fontInputValue.trim().toLowerCase() : '' + ); + const matchesFilter = (f: string) => !fontFilter || f.toLowerCase().includes(fontFilter); + let recentShown = $derived(recentFonts.filter(matchesFilter)); + let webSafeShown = $derived(WEB_SAFE_FONTS.filter(matchesFilter)); + let extraShown = $derived(extraFontsList.filter(matchesFilter)); + function openFontPicker() { if (!editor) return; sizeOpen = false; @@ -330,9 +347,36 @@ fontOpen = !fontOpen; } + function onFontInputFocus(e: FocusEvent) { + fontInputFocused = true; + if (!fontOpen) openFontPicker(); + (e.target as HTMLInputElement).select(); + } + + function onFontInputKeydown(e: KeyboardEvent) { + if (e.key === 'Enter') { + e.preventDefault(); + const typed = fontInputValue.trim(); + const known = [...recentFonts, ...WEB_SAFE_FONTS, ...extraFontsList]; + // Exact name wins; otherwise the first prefix match, like LibreOffice's + // autocomplete. An unknown name is applied as typed (the font may exist). + const hit = known.find(f => f.toLowerCase() === typed.toLowerCase()) + ?? known.find(f => f.toLowerCase().startsWith(typed.toLowerCase())); + if (typed) pickFont(hit ?? typed); + (e.target as HTMLInputElement).blur(); + } else if (e.key === 'Escape') { + e.preventDefault(); + fontOpen = false; + fontInputFocused = false; + (e.target as HTMLInputElement).blur(); + editor?.commands.focus(); + } + } + function pickFont(value: string) { if (!editor) return; fontOpen = false; + fontInputFocused = false; const from = savedFrom ?? editor.state.selection.from; const to = savedTo ?? editor.state.selection.to; savedFrom = null; @@ -947,18 +991,31 @@
{#if editor}
- +
+ (fontInputFocused = false)} + spellcheck="false" + autocomplete="off" + title={t().toolbarExpanded.fontName} + /> + +
{#if fontOpen}
- {#each WEB_SAFE_FONTS as font} + {#if webSafeShown.length > 0} + + {/if} + {#each webSafeShown as font} {/each} - {#if extraFontsList.length > 0} + {#if extraShown.length > 0} - {#each extraFontsList as font} + {#each extraShown as font}