From 3b299809e1d053f80b84ae16d803c3a1a790cde7 Mon Sep 17 00:00:00 2001 From: Steffen Becker Date: Sun, 28 Jun 2026 00:05:37 +0200 Subject: [PATCH] feat: search and replace --- src/App.svelte | 45 ++++- src/lib/components/FindReplaceBar.svelte | 180 ++++++++++++++++++ src/lib/components/LinkDialog.svelte | 3 +- src/lib/editor/extensions.ts | 2 + src/lib/editor/extensions/searchReplace.ts | 210 +++++++++++++++++++++ src/styles/editor.css | 9 + tests/unit/search-replace.test.ts | 43 +++++ 7 files changed, 490 insertions(+), 2 deletions(-) create mode 100644 src/lib/components/FindReplaceBar.svelte create mode 100644 src/lib/editor/extensions/searchReplace.ts create mode 100644 tests/unit/search-replace.test.ts diff --git a/src/App.svelte b/src/App.svelte index 1f10e97..9d692d9 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -5,6 +5,7 @@ import EditorComponent from './lib/components/Editor.svelte'; import Toolbar from './lib/components/Toolbar.svelte'; import ToolbarExpanded from './lib/components/ToolbarExpanded.svelte'; + import FindReplaceBar from './lib/components/FindReplaceBar.svelte'; import { buildOdt, deriveFilename } from './lib/export/odt'; import { exportPdf, printPdf } from './lib/export/pdf'; import { supportsFsAccess, saveOdt, saveAsOdt, openOdt } from './lib/export/saveFile'; @@ -40,6 +41,24 @@ let activeEditor = $derived(hfActive ? hfEditor : editor); let activeTick = $derived(hfActive ? hfTick : tick); + // Find & Replace bar (searchReplace.ts). Targets the body editor; positioned just + // below the toolbar region (toolbarRegionH tracks its height, expanded or not). + let findOpen = $state(false); + let findMode: 'find' | 'replace' = $state('find'); + let findNonce = $state(0); + let toolbarRegionH = $state(0); + + function openFind(mode: 'find' | 'replace') { + findMode = mode; + findOpen = true; + findNonce++; + } + + function closeFind() { + findOpen = false; + editor?.commands.clearSearch(); + } + // Word/character counts for the status-bar counter. Reading `tick` makes these // recompute on every body transaction (incl. selection changes), so they stay // live without subscribing to ProseMirror directly. @@ -441,6 +460,17 @@ e.preventDefault(); handlePrintPdf(); } + // Ctrl/Cmd+F → Find, Ctrl/Cmd+H → Find & Replace (suppress the browser's own find). + if ((e.ctrlKey || e.metaKey) && !e.shiftKey && (e.key === 'f' || e.key === 'F')) { + e.preventDefault(); + openFind('find'); + } + if ((e.ctrlKey || e.metaKey) && !e.shiftKey && (e.key === 'h' || e.key === 'H')) { + e.preventDefault(); + openFind('replace'); + } + // Escape closes the bar (when it isn't handled inside an input). + if (e.key === 'Escape' && findOpen) closeFind(); } window.addEventListener('keydown', onKeydown); return () => window.removeEventListener('keydown', onKeydown); @@ -471,7 +501,7 @@
-
+
@@ -706,6 +736,11 @@ {pageMargins} orientation={pageOrientation} /> + {#if findOpen && editor} +
+ +
+ {/if}