From 1fb81c389f8c9d2ea95d84dfa9e5ce6dfe3bec25 Mon Sep 17 00:00:00 2001 From: Steffen Becker Date: Wed, 8 Jul 2026 23:11:54 +0200 Subject: [PATCH] feat: insert date/time --- CHANGELOG.md | 1 + src/lib/components/DateTimePicker.svelte | 213 +++++++++++++++++++++ src/lib/components/ToolbarExpanded.svelte | 46 ++++- src/lib/editor/extensions.ts | 4 + src/lib/editor/extensions/dateTimeField.ts | 94 +++++++++ src/lib/export/docx.ts | 28 ++- src/lib/export/odt.ts | 94 ++++++++- src/lib/i18n/locales/de.ts | 7 + src/lib/i18n/locales/en.ts | 7 + src/lib/import/docx.ts | 57 +++++- src/lib/import/odt.ts | 81 +++++++- src/lib/import/styleResolver.ts | 10 + src/lib/utils/dateTime.ts | 180 +++++++++++++++++ tests/docx-roundtrip.test.ts | 29 +++ tests/roundtrip.test.ts | 47 +++++ 15 files changed, 882 insertions(+), 16 deletions(-) create mode 100644 src/lib/components/DateTimePicker.svelte create mode 100644 src/lib/editor/extensions/dateTimeField.ts create mode 100644 src/lib/utils/dateTime.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 78433af..bb59b9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ - Table border control (Word/LibreOffice-style): per-side cell borders with presets (all / outside / inside / single edges / none), line width and color; buttons show active states matching the current pen and toggle borders off; round-trips to ODF `fo:border-*` and DOCX `w:tcBorders` - Images: inline or floating with text wrap (left / right / top-bottom), resize handles, rotation, live size badge; insert via toolbar, drag-and-drop, or paste - Special characters picker +- Date and time fields: picker with 7 date and 4 time formats (live samples) and an "update automatically" toggle — fixed fields keep the inserted moment, auto fields refresh on open. Round-trips to ODF `text:date`/`text:time` (minted `number:date/time-style`) and DOCX `DATE`/`TIME` fields; the field carries the surrounding font - Table of contents: generated from headings (H1–H3) with live page numbers and dot leaders, click an entry to jump to its heading; round-trips to ODF `text:table-of-content` and a Word TOC field - Hyperlinks: create / edit / remove (toolbar + Ctrl+K), Ctrl/Cmd+click to open, hover hint showing the URL; ODF `text:a` round-trip - Manual page break (Ctrl+Enter); round-trips to ODF `fo:break-before` diff --git a/src/lib/components/DateTimePicker.svelte b/src/lib/components/DateTimePicker.svelte new file mode 100644 index 0000000..aa46036 --- /dev/null +++ b/src/lib/components/DateTimePicker.svelte @@ -0,0 +1,213 @@ + + +
+ + + {#if open} + + {/if} +
+ + diff --git a/src/lib/components/ToolbarExpanded.svelte b/src/lib/components/ToolbarExpanded.svelte index 2558821..1f92787 100644 --- a/src/lib/components/ToolbarExpanded.svelte +++ b/src/lib/components/ToolbarExpanded.svelte @@ -4,6 +4,7 @@ import ColorPicker from './ColorPicker.svelte'; import TablePicker from './TablePicker.svelte'; import SpecialCharPicker from './SpecialCharPicker.svelte'; + import DateTimePicker from './DateTimePicker.svelte'; import LinkDialog from './LinkDialog.svelte'; import { OPEN_LINK_DIALOG_EVENT } from '../editor/extensions/link'; import { @@ -100,6 +101,12 @@ if (list && list.length > 0) allInstalledFonts = list; } + // A node that carries text formatting in the selection: a text node, or an inline + // atom (e.g. a date/time field) bearing the mark in question. Selecting such an atom + // makes a NodeSelection, so without this its font/size/color would read as default. + const bearsMark = (node: { isText: boolean; isInline: boolean; isAtom: boolean; marks: readonly { type: { name: string } }[] }, markName: string): boolean => + node.isText || (node.isInline && node.isAtom && node.marks.some(m => m.type.name === markName)); + // Returns the uniform font of the selection, or '' when fonts are mixed. // Plain Text without an explicit mark falls back to DEFAULT_EDITOR_FONT. let currentFont = $derived.by(() => { @@ -112,7 +119,7 @@ let font: string | undefined; let mixed = false; editor.state.doc.nodesBetween(from, to, (node) => { - if (mixed || !node.isText) return; + if (mixed || !bearsMark(node, 'textStyle')) return; const f: string = node.marks.find(m => m.type.name === 'textStyle')?.attrs.fontFamily ?? DEFAULT_EDITOR_FONT; if (font === undefined) font = f; else if (font !== f) mixed = true; @@ -147,7 +154,7 @@ let size: string | undefined; let mixed = false; editor.state.doc.nodesBetween(from, to, (node, _pos, parent) => { - if (mixed || !node.isText) return; + if (mixed || !bearsMark(node, 'textStyle')) return; const s = effectiveSize(node as Parameters[0], parent as Parameters[1]); if (size === undefined) size = s; else if (size !== s) mixed = true; @@ -252,7 +259,7 @@ let color: string | null | undefined; let mixed = false; editor.state.doc.nodesBetween(from, to, (node) => { - if (mixed || !node.isText) return; + if (mixed || !bearsMark(node, markName)) return; const c = node.marks.find(m => m.type.name === markName)?.attrs.color ?? null; if (color === undefined) color = c; else if (color !== c) mixed = true; @@ -766,6 +773,7 @@ layoutOpen = false; tableOpen = false; columnsOpen = false; + dateTimeOpen = false; } function insertSpecialChar(char: string, range: { from: number; to: number }) { @@ -773,6 +781,32 @@ editor.chain().focus().setTextSelection(range).insertContent(char).run(); } + // --- Date/time field insertion (DateTimePicker.svelte) --- + let dateTimeOpen = $state(false); + + // Close the sibling dropdowns (incl. the special-char picker) without touching + // dateTimeOpen, so the trigger's own open/close toggle keeps working. + function onDateTimePickerOpen() { + fontOpen = false; + sizeOpen = false; + sizeInputFocused = false; + lineHeightOpen = false; + fontColorOpen = false; + highlightColorOpen = false; + layoutOpen = false; + tableOpen = false; + columnsOpen = false; + specialCharOpen = false; + } + + function insertDateTime( + opts: { kind: 'date' | 'time'; format: string; fixed: boolean }, + range: { from: number; to: number }, + ) { + if (!editor) return; + editor.chain().focus().setTextSelection(range).insertDateTimeField(opts).run(); + } + // --- Image insertion (image.ts) --- let imageInput = $state(null); @@ -1302,6 +1336,12 @@ onOpen={onSpecialCharPickerOpen} onInsert={insertSpecialChar} /> +