diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f8c94d..121b408 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ **Page & layout** - Page margins (cm) and page orientation (portrait / landscape) +- Multi-column (newspaper) layout: 1–3 columns with adjustable gap, applied to the whole document (no selection) or to selected paragraphs, Word-style — text fills column 1 to the page bottom, then column 2, and flows across pages with mid-paragraph line breaks; a section followed by other content balances its columns. Round-trips to ODF `text:section`/`style:columns` and DOCX continuous sections with `w:cols` - Headers & footers with page-number / page-count fields and configurable edge distances - Zoom (20–300 %) - Show formatting marks (spaces, tabs, paragraph marks) @@ -81,7 +82,6 @@ Planned word-processor features (to match Word/LibreOffice): - Footnotes / endnotes (currently dropped on import) - Clear formatting (remove all marks) -- Multi-column (newspaper) layout - Comments / annotations (currently dropped on import) - Horizontal rule - Page numbering options (start value / format) + different first-page header diff --git a/src/App.svelte b/src/App.svelte index 33b2fb8..f0da589 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -12,6 +12,7 @@ import { importOdt } from './lib/import/odt'; import { importDocx } from './lib/import/docx'; import { getPageBreakDebug } from './lib/editor/extensions/pageBreaks'; + import { getColumnsFlowDebug } from './lib/editor/extensions/columnsFlow'; import { getColorDebug } from './lib/utils/colorDebug'; import { countText, type TextStats } from './lib/utils/wordCount'; import { loadTheme, saveTheme, applyTheme, loadToolbarExpanded, saveToolbarExpanded, loadFormattingMarks, saveFormattingMarks, type ThemeMode } from './lib/storage/theme'; @@ -557,6 +558,7 @@ zoom, doc: editor.getJSON(), pageBreaks: snapshot, + columnsFlow: getColumnsFlowDebug(editor.view), colors: getColorDebug(editor), }; const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }); diff --git a/src/lib/components/ToolbarExpanded.svelte b/src/lib/components/ToolbarExpanded.svelte index 4fc025b..8d70c44 100644 --- a/src/lib/components/ToolbarExpanded.svelte +++ b/src/lib/components/ToolbarExpanded.svelte @@ -16,6 +16,7 @@ import type { Orientation } from '../storage/pageOrientation'; import { DEFAULT_HF_DISTANCES, clampHfDistance, type HfDistances } from '../storage/headerFooter'; import { listContext } from '../editor/extensions/indent'; + import { findColumns, DEFAULT_COLUMN_GAP_CM } from '../editor/extensions/columns'; import { t } from '../i18n/i18n.svelte'; import { withShortcut } from '../i18n/shortcut'; @@ -612,6 +613,7 @@ lineHeightOpen = false; fontColorOpen = false; highlightColorOpen = false; + columnsOpen = false; layoutOpen = !layoutOpen; if (layoutOpen) { syncMarginInputs(); @@ -635,6 +637,7 @@ sizeOpen = false; sizeInputFocused = false; lineHeightOpen = false; + columnsOpen = false; if (which === 'font') highlightColorOpen = false; else fontColorOpen = false; } @@ -652,6 +655,7 @@ highlightColorOpen = false; layoutOpen = false; specialCharOpen = false; + columnsOpen = false; } function insertTable(rows: number, cols: number, range: { from: number; to: number }) { @@ -665,6 +669,86 @@ editor.chain().focus().setTableOfContents().run(); } + // --- Multi-column section (columns.ts) --- + let columnsOpen = $state(false); + let columnGapInput = $state(''); + + // Count/gap of the columns section around the cursor; count 1 = not in one. + let colState = $derived.by(() => { + if (tick < 0 || !editor) return { count: 1, gapCm: DEFAULT_COLUMN_GAP_CM, inColumns: false }; + const found = findColumns(editor.state); + return found + ? { count: found.node.attrs.count as number, gapCm: found.node.attrs.gapCm as number, inColumns: true } + : { count: 1, gapCm: DEFAULT_COLUMN_GAP_CM, inColumns: false }; + }); + + function openColumnsPicker() { + fontOpen = false; + sizeOpen = false; + sizeInputFocused = false; + lineHeightOpen = false; + fontColorOpen = false; + highlightColorOpen = false; + layoutOpen = false; + tableOpen = false; + specialCharOpen = false; + columnsOpen = !columnsOpen; + if (columnsOpen) columnGapInput = fmtCm(colState.gapCm); + } + + function columnsClickOutside(node: HTMLElement) { + function handler(e: MouseEvent) { + if (!node.contains(e.target as Node)) columnsOpen = false; + } + window.addEventListener('mousedown', handler); + return { destroy() { window.removeEventListener('mousedown', handler); } }; + } + + // The current gap straight from the document (colState may not have recomputed yet + // within the same handler after a command ran). + function liveColumnGap(): number { + const found = editor ? findColumns(editor.state) : null; + return (found?.node.attrs.gapCm as number | undefined) ?? DEFAULT_COLUMN_GAP_CM; + } + + function applyColumnCount(n: number) { + if (!editor || hfActive) return; + editor.chain().focus().setColumns(n).run(); + columnGapInput = fmtCm(liveColumnGap()); + } + + function setColumnGap(value: number) { + if (!editor) return; + editor.chain().focus().setColumnGap(value).run(); + columnGapInput = fmtCm(liveColumnGap()); + } + + function commitColumnGapInput() { + const n = parseFloat(columnGapInput); + if (Number.isNaN(n)) { + columnGapInput = fmtCm(liveColumnGap()); // revert invalid input + return; + } + setColumnGap(n); + } + + function onColumnGapKeydown(e: KeyboardEvent) { + if (e.key === 'Enter') { + e.preventDefault(); + commitColumnGapInput(); + (e.target as HTMLInputElement).blur(); + } else if (e.key === 'Escape') { + columnGapInput = fmtCm(liveColumnGap()); + (e.target as HTMLInputElement).blur(); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + setColumnGap(liveColumnGap() + MARGIN_STEP); + } else if (e.key === 'ArrowDown') { + e.preventDefault(); + setColumnGap(liveColumnGap() - MARGIN_STEP); + } + } + // --- Special character insertion (SpecialCharPicker.svelte) --- let specialCharOpen = $state(false); @@ -678,6 +762,7 @@ highlightColorOpen = false; layoutOpen = false; tableOpen = false; + columnsOpen = false; } function insertSpecialChar(char: string, range: { from: number; to: number }) { @@ -1238,6 +1323,95 @@ +
+ + {#if columnsOpen} +
+ +
+ + + +
+ +
+
+ +
+ + +
+
+
+
+ {/if} +