From 1ad802943dc41e81f3c445a97a58557359e992a1 Mon Sep 17 00:00:00 2001 From: Steffen Becker Date: Tue, 23 Jun 2026 02:31:39 +0200 Subject: [PATCH] feat: word counter --- src/App.svelte | 140 +++++++++++++++++++++++++++++++++++- src/lib/editor/wordCount.ts | 31 ++++++++ 2 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 src/lib/editor/wordCount.ts diff --git a/src/App.svelte b/src/App.svelte index 5b31317..46287db 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -11,6 +11,7 @@ import { importOdt } from './lib/import/odt'; import { getPageBreakDebug } from './lib/editor/pageBreaks'; import { getColorDebug } from './lib/editor/colorDebug'; + import { countText, type TextStats } from './lib/editor/wordCount'; import { loadTheme, saveTheme, applyTheme, loadToolbarExpanded, saveToolbarExpanded, loadFormattingMarks, saveFormattingMarks, type ThemeMode } from './lib/storage/theme'; import { loadPageMargins, savePageMargins, DEFAULT_MARGINS, type PageMargins } from './lib/storage/pageMargins'; import { loadOrientation, saveOrientation, type Orientation } from './lib/storage/pageOrientation'; @@ -37,6 +38,21 @@ let activeEditor = $derived(hfActive ? hfEditor : editor); let activeTick = $derived(hfActive ? hfTick : tick); + // 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. + let wordCountOpen = $state(false); + let docStats = $derived.by(() => { + if (tick < 0 || !editor) return { words: 0, charsWithSpaces: 0, charsNoSpaces: 0, paragraphs: 0 }; + const { doc } = editor.state; + return countText(doc, 0, doc.content.size); + }); + let selStats = $derived.by(() => { + if (tick < 0 || !editor) return null; + const { from, to, empty } = editor.state.selection; + return empty ? null : countText(editor.state.doc, from, to); + }); + let themeMode: ThemeMode = $state(loadTheme()); let themeOpen = $state(false); let toolbarExpanded = $state(loadToolbarExpanded()); @@ -323,6 +339,14 @@ return { destroy() { window.removeEventListener('mousedown', handler); } }; } + function wordCountClickOutside(node: HTMLElement) { + function handler(e: MouseEvent) { + if (!node.contains(e.target as Node)) wordCountOpen = false; + } + window.addEventListener('mousedown', handler); + return { destroy() { window.removeEventListener('mousedown', handler); } }; + } + onMount(() => { function onKeydown(e: KeyboardEvent) { // Ctrl/Cmd+S → Save (suppress the browser's save-page dialog). @@ -560,7 +584,42 @@ orientation={pageOrientation} />