docs: remove word as placeholder for general text editor

This commit is contained in:
Steffen Becker committed 2026-08-07 18:08:20 +02:00
1 parent ad38994c65
commit 7c09727cda
18 files changed
+33 -33

No files matched your search

+3 -3
View File
@@ -48,7 +48,7 @@
- Bulleted and ordered lists — nested, with multiple numbering styles (decimal, alpha, roman; `.` or `)` suffix) and whole-list indent; Tab / Shift-Tab to nest / un-nest
- Word-style nesting defaults for ordered lists: an indented level numbers 1. → a. → i. (repeating) instead of restarting at "1." everywhere; explicit styles per level still override. Plus legal/outline numbering (1., 1.1., 1.2.1. …) as a list type — rendered via CSS counters, round-trips to ODF `text:display-levels` and DOCX `%1.%2.` lvlText chains
- Nested ordered levels advance the cycle relative to the level above and inherit its suffix (a level-1 `a)` gives `i)` then `1)`); the numbering style chosen for a level is reused when you nest into it again
- Customizable bullet symbols per list level (Word-style picker on the bullet-list split button: • ◦ ▪ ❖ ➢ ⇨ ✓ – >); round-trips to ODF `text:bullet-char` and DOCX `w:lvlText`, and the DOCX/ODT import maps Wingdings/Symbol bullets (arrows, diamonds, checkmarks …) to their Unicode equivalents instead of flattening them to plain dots. Symbols Liberation Serif lacks render from a bundled 2 KB DejaVu Sans subset (`EdenSymbols.woff2`), so markers look compact and identical on every platform instead of a stretched OS fallback
- Customizable bullet symbols per list level (picker on the bullet-list split button: • ◦ ▪ ❖ ➢ ⇨ ✓ – >); round-trips to ODF `text:bullet-char` and DOCX `w:lvlText`, and the DOCX/ODT import maps Wingdings/Symbol bullets (arrows, diamonds, checkmarks …) to their Unicode equivalents instead of flattening them to plain dots. Symbols Liberation Serif lacks render from a bundled 2 KB DejaVu Sans subset (`EdenSymbols.woff2`), so markers look compact and identical on every platform instead of a stretched OS fallback
- Tab stops per paragraph: left, centre, right and decimal, honoured in the rendered text (CSS only has a fixed tab grid, so each tab is measured and placed); a hanging indent implies a stop at the text position. Set them on the ruler; round-trips to ODF `style:tab-stops` and DOCX `w:tabs`
- Manual line breaks (Shift+Enter)
@@ -67,7 +67,7 @@
- Page margins (cm) and page orientation (portrait / landscape)
- Page format picker (Layout panel): 15 Word/LibreOffice sizes — A3–A6, ISO B4–B6, JIS B4/B5, Letter, Legal, Tabloid, Executive, Folio, Statement — each showing its cm dimensions; drives the on-screen page, pagination, and ODT/DOCX/PDF export, and is detected & adopted on import
- 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: page-number / page-count fields, configurable edge distances, and Word-style variants — different first page and different odd & even pages (edit a page's zone directly; blank first/even zones supported). Press Enter to add blank lines that grow a zone into the page while body text reflows so the page break sits above the footer / below the header, never overlapping. Inline images/logos can be inserted in a zone. Round-trips to ODF (`style:header-first`/`-left`, `draw:frame`) and DOCX (`w:titlePg`/`w:evenAndOddHeaders`, `ImageRun`)
- Headers & footers: page-number / page-count fields, configurable edge distances, and variants — different first page and different odd & even pages (edit a page's zone directly; blank first/even zones supported). Press Enter to add blank lines that grow a zone into the page while body text reflows so the page break sits above the footer / below the header, never overlapping. Inline images/logos can be inserted in a zone. Round-trips to ODF (`style:header-first`/`-left`, `draw:frame`) and DOCX (`w:titlePg`/`w:evenAndOddHeaders`, `ImageRun`)
- Zoom (20–300 %)
- Horizontal ruler above the page: click to place a tab stop of the selected type, drag to move it, drag it off to remove it; two markers set the paragraph's first-line and left indent. Toggle in the extended toolbar
- Show formatting marks (spaces, tabs, paragraph marks)
@@ -76,7 +76,7 @@
- Word/LibreOffice keyboard shortcuts throughout: Ctrl+L/E/R/J alignment, Ctrl+Alt+1–5 headings and Ctrl+Shift+N default style, Ctrl+1/2/5 line spacing, Ctrl+Shift+P / Ctrl+Shift+B super/subscript, Ctrl+Shift+. / Ctrl+Shift+, grow/shrink font, Ctrl+Space clear formatting, Ctrl+Shift+Space non-breaking space, Ctrl+Shift+- soft hyphen, Alt+Shift+D / Alt+Shift+T date & time field, Ctrl+O open, F3 / Shift+F3 find next/previous, Ctrl+F10 formatting marks — alongside the existing Ctrl+B/I/U, Ctrl+M, Ctrl+K, Ctrl+Enter, Ctrl+F/H, Ctrl+S/P and Tab/Shift+Tab. All bindings live in one table, so tooltips stay in sync and they can be remapped later
- Undo / Redo with a labelled history dropdown
- Search & Replace (Ctrl+F / Ctrl+H): live match highlighting, match count, next / previous, match-case and whole-word options, replace current / replace all
- Right-click context menu for text (Word-style): cut / copy / paste / paste without formatting, link insert-edit-remove, clear formatting — with the spelling suggestions merged in on top. Shift+right-click keeps the browser's own menu; images, text boxes and header/footer stay with their existing UI
- Right-click context menu for text: cut / copy / paste / paste without formatting, link insert-edit-remove, clear formatting — with the spelling suggestions merged in on top. Shift+right-click keeps the browser's own menu; images, text boxes and header/footer stay with their existing UI
- Spell check in English and German with squiggles and suggestions in the context menu (add / ignore word); selectable document language. Powered by Hunspell (WASM), so German compound words (Fußgänger, Krankenversicherung …) are recognised; dictionaries are lazy-loaded per language on demand
- Word / character count statistics (whole document and selection)
+6 -6
View File
@@ -61,8 +61,8 @@
});
// A header/footer that reaches past the body's margin (its distance from the edge plus
// its line count) pushes the body's content area in so text doesn't overlap it — Word
// grows the margin to fit the zone. ~18.4px = one 12pt line.
// its line count) pushes the body's content area in so text doesn't overlap it: the
// margin grows to fit the zone. ~18.4px = one 12pt line.
const HF_LINE_PX = 16 * 1.15;
function hfReachPx(doc: HfDoc, distPx: number): number {
if (!doc || hfIsEmpty(doc)) return 0;
@@ -193,7 +193,7 @@
return (Number.isFinite(ph) ? ph : 1123) + 20;
}
// --- Right-click context menu (Word's text menu, spelling suggestions merged in) ---
// --- Right-click context menu (text menu, spelling suggestions merged in) ---
let ctxMenu = $state<{ top: number; left: number; items: MenuEntry[] } | null>(null);
// The misspelled range the open menu's suggestions belong to.
let spellTarget: { from: number; to: number; word: string } | null = null;
@@ -212,7 +212,7 @@
const view = ed.view;
const coords = view.posAtCoords({ left: event.clientX, top: event.clientY });
const sel = view.state.selection;
// Word keeps a selection the click lands inside, otherwise it moves the caret.
// A selection the click lands inside is kept, otherwise the caret moves.
if (coords && (sel.empty || coords.pos < sel.from || coords.pos > sel.to)) {
view.dispatch(view.state.tr.setSelection(TextSelection.near(view.state.doc.resolve(coords.pos))));
}
@@ -294,7 +294,7 @@
// Shown when the selection is inside a table; positioned just above that table.
let tableUi = $state<{ visible: boolean; top: number; left: number }>({ visible: false, top: 0, left: 0 });
let tableUiRaf = 0;
// Word-style "Split Cells…" popover, opened from the table toolbar.
// "Split Cells…" popover, opened from the table toolbar.
let splitDialogOpen = $state(false);
// Drop the dialog if the selection leaves the table (toolbar hidden).
$effect(() => {
@@ -738,7 +738,7 @@
saveDocument(e.getJSON());
},
onFocus: () => {
// Clicking back into the body ends header/footer editing (Word behaviour).
// Clicking back into the body ends header/footer editing.
hfActive = null;
},
});
+1 -1
View File
@@ -307,7 +307,7 @@
flex-direction: column;
pointer-events: auto;
/* Content taller than the margin band spills into the margin (footer up, header
down), like Word's auto-growing zones; the anchored edge stays put. */
down) — the zone auto-grows; the anchored edge stays put. */
overflow: visible;
font-family: var(--font-serif);
font-size: 12pt;
@@ -6,7 +6,7 @@
let { editor, tick }: { editor: Editor | null; tick: number } = $props();
let open = $state(false);
// Pen settings (Word-style): presets apply with the currently chosen width/color.
// Pen settings: presets apply with the currently chosen width/color.
let widthPt = $state(0.5);
let color = $state('#000000');
+2 -2
View File
@@ -6,7 +6,7 @@
let { editor, tick }: { editor: Editor | null; tick: number } = $props();
let open = $state(false);
// Pen settings (Word-style): presets apply with the currently chosen width/color.
// Pen settings: presets apply with the currently chosen width/color.
let widthPt = $state(0.5);
let color = $state('#000000');
@@ -45,7 +45,7 @@
);
// Clicking an inactive preset applies the pen; clicking an active one toggles those
// borders off (Word behavior). The panel stays open so the states visibly update.
// borders off. The panel stays open so the states visibly update.
function apply(id: BorderPreset | 'none') {
if (!editor) return;
if (id === 'none') editor.chain().focus().setTableBorders('all', null).run();
+1 -1
View File
@@ -189,7 +189,7 @@
user-select: none;
}
/* Tiles and options side by side, as in Word's Table Design tab. */
/* Tiles and options side by side. */
.tsp-body {
display: flex;
align-items: flex-start;
+1 -1
View File
@@ -274,7 +274,7 @@
return mixed ? '' : (c ?? null);
});
// Sub/superscript are mutually exclusive (Word-style): toggling one clears the
// Sub/superscript are mutually exclusive: toggling one clears the
// other. odf-kit maps these marks to text:position (round-trips with LibreOffice
// and Word). See export/odt.ts applyRuns for the custom-attr-paragraph path.
let isSuperscript = $derived(tick >= 0 && !!editor?.isActive('superscript'));
+1 -1
View File
@@ -4,7 +4,7 @@ import { withShortcut } from '../i18n/shortcut';
import { shortcutHint } from './shortcuts';
import { OPEN_LINK_DIALOG_EVENT } from './extensions/link';
// The right-click menu's contents, Word's text menu mapped onto this editor. Pure
// The right-click menu's contents, the usual text menu mapped onto this editor. Pure
// data + closures — ContextMenu.svelte only renders it.
export type MenuEntry =
| { kind: 'sep' }
+2 -2
View File
@@ -123,11 +123,11 @@ export const extensions = [
TableCell,
TableColumnResize,
TableRowResize,
// Word-style "Split Cells…" (N×M); merge uses extension-table's built-in mergeCells.
// "Split Cells…" (N×M); merge uses extension-table's built-in mergeCells.
TableSplit,
// Cell shading: backgroundColor attr on tableCell/tableHeader (→ fo:background-color).
TableCellBackground,
// Word-style border presets: per-side border attrs on cells (→ fo:border-*).
// Border presets: per-side border attrs on cells (→ fo:border-*).
TableCellBorders,
// "Header row" styling preset: bold + light shading on the first row (toggle).
TableHeaderRow,
+2 -2
View File
@@ -125,7 +125,7 @@ export function mergeJoinedBlocks(blocks: PMNode[]): PMNode[] {
return out;
}
// Word's no-selection behavior: apply columns to the whole document. Wrappable runs
// With no selection, columns apply to the whole document. Wrappable runs
// (absorbing existing sections) become one section each; tables/boxes/TOCs stay
// between them. Replacements run bottom-up so earlier positions stay valid.
function applyToWholeDoc(state: EditorState, tr: Transaction, attrs: ColumnsAttrs): boolean {
@@ -221,7 +221,7 @@ export const Columns = Node.create({
return {
// Inside a section: 1 unwraps the whole chain, 2/3 update its count. Outside:
// with a selection, 2/3 wrap the covered top-level blocks (fails on tables
// etc.); with a bare cursor, the whole document gets columns (Word behavior).
// etc.); with a bare cursor, the whole document gets columns.
setColumns:
(count: number) =>
({ state, dispatch }) => {
+2 -2
View File
@@ -197,7 +197,7 @@ export const ColumnsFlow = Extension.create({
return open;
}
// Split the paragraph at `blockIndex` at the line consuming `budgetPx` (Word's
// Split the paragraph at `blockIndex` at the line consuming `budgetPx` (a
// mid-paragraph page break); the second part is marked joinPrev so joins/export
// restore the original. `force` takes at least one line for an over-tall paragraph.
function splitParagraphInBlock(
@@ -289,7 +289,7 @@ export const ColumnsFlow = Extension.create({
if (first + COLUMNS_FIT_MARGIN_PX > available) {
// First block alone doesn't fit. Mid-page, pageBreaks pushes the
// fragment; at a page top pushing can't help — split the paragraph
// at a line boundary instead (Word's mid-paragraph page break).
// at a line boundary instead (a mid-paragraph page break).
if (
available >= vm.contentHeight - 1 &&
splitParagraphInBlock(frags[i], children, 0, count * (available - SAFETY_PX), scale, true)
+1 -1
View File
@@ -28,7 +28,7 @@ import { Shortcuts } from './shortcuts';
const HfDocument = Document.extend({ content: 'paragraph' });
// The single-paragraph schema can't split, so plain Enter inserts a line break instead
// (Word: Enter in a footer adds a blank line, growing the zone into the page). Shift-/
// (Enter in a footer adds a blank line, growing the zone into the page). Shift-/
// Mod-Enter keep their default break binding.
const HfHardBreak = HardBreak.extend({
addKeyboardShortcuts() {
+1 -1
View File
@@ -23,7 +23,7 @@ declare module '@tiptap/core' {
export const MIN_SIZE_PX = 24;
// Corners keep aspect (Word-style); edges (n/s/e/w) change one dimension only.
// Corners keep aspect; edges (n/s/e/w) change one dimension only.
// Shared with textBox.ts, whose node view uses the same handle set.
export const HANDLES: { k: string; x: -1 | 0 | 1; y: -1 | 0 | 1; aspect: boolean }[] = [
{ k: 'nw', x: -1, y: -1, aspect: true },
+4 -4
View File
@@ -46,7 +46,7 @@ function innermostList($from: ResolvedPos): { node: PMNode; depth: number; depth
}
// The type-defining attr an explicit same-kind list at `depth0` already uses in this
// tree (DFS-first), so a freshly nested list can reuse it — Word's per-level memory.
// tree (DFS-first), so a freshly nested list can reuse it — the per-level memory.
function rememberedListStyle(root: PMNode, kind: string, depth0: number): Record<string, unknown> | null {
let found: Record<string, unknown> | null = null;
const walk = (list: PMNode, d: number) => {
@@ -104,7 +104,7 @@ function clampIndent(cm: number): number {
return Math.round(Math.max(0, Math.min(INDENT_MAX_CM, cm)) * 100) / 100;
}
// A first-line indent is signed (negative = Word's hanging indent).
// A first-line indent is signed (negative = a hanging indent).
function clampFirst(cm: number): number {
return Math.round(Math.max(-INDENT_MAX_CM, Math.min(INDENT_MAX_CM, cm)) * 100) / 100;
}
@@ -115,7 +115,7 @@ function parseIndent(value: string | null): number | null {
return Number.isFinite(n) && n > 0 ? n : null;
}
// A first-line indent may be negative (Word's hanging indent), unlike a left margin.
// A first-line indent may be negative (a hanging indent), unlike a left margin.
function parseSignedIndent(value: string | null): number | null {
if (value == null || value === '') return null;
const n = parseFloat(value);
@@ -153,7 +153,7 @@ export const Indent = Extension.create({
};
},
},
// First-line indent in cm; negative is Word's hanging indent. Independent of
// First-line indent in cm; negative is a hanging indent. Independent of
// `indent`, which is the whole block's left margin.
indentFirst: {
default: null,
+1 -1
View File
@@ -9,7 +9,7 @@ import { columnWeightsFromRow } from './tableView';
// cell selection) into `cols` × `rows` sub-cells. prosemirror-tables only ships
// splitCell (un-merge), so this is custom. The table is a strict rectangular grid,
// so adding sub-columns/rows means inserting grid lines and letting every other
// cell that crosses a new line grow its span (bridge) — exactly what Word does.
// cell that crosses a new line grow its span (bridge).
// Implemented by reading the grid via TableMap, expanding it, partitioning the
// cell's region, and rebuilding the table node in one step.
+1 -1
View File
@@ -364,7 +364,7 @@ class TextBoxView {
);
}
// Word-like frame hit test: the border plus a few px of the inner padding ring
// Frame hit test: the border plus a few px of the inner padding ring
// select the box; anywhere further inside is text area. Rotation-aware (the point
// is un-rotated into the rotor's own axes) and zoom-aware.
private isFrameHit(e: MouseEvent): boolean {
+1 -1
View File
@@ -25,7 +25,7 @@ export interface DtFormat {
tokens: Token[];
}
// Word-style catalog. Samples in the picker render the current time, so the exact
// Format catalog. Samples in the picker render the current time, so the exact
// ordering/separators are visible when choosing.
export const DATE_FORMATS: DtFormat[] = [
{ key: 'iso', kind: 'date', tokens: [
+2 -2
View File
@@ -280,7 +280,7 @@
.paper .tiptap .image-resize-sw { bottom: -5px; left: -5px; cursor: nesw-resize; }
.paper .tiptap .image-resize-w { top: 50%; left: -5px; margin-top: -5px; cursor: ew-resize; }
/* Rotate grip floats above top-centre with a short connector line; the icon is a
circular rotation arrow (like Word). */
circular rotation arrow. */
.paper .tiptap .image-rotate-handle {
display: none;
position: absolute;
@@ -579,7 +579,7 @@
height: 0;
}
/* Paragraph mark (). Reserved in the layout at all times (Word's model) so the toggle
/* Paragraph mark (). Reserved in the layout at all times, so the toggle
only recolors it and text never reflows. Zero-size inline-block, glyph overflows
visibly: width/height/line-height 0 drop both footprint and line-box strut. */
.paper .tiptap p::after,