docs: remove word as placeholder for general text editor
This commit is contained in:
1 parent
ad38994c65
commit
7c09727cda
18 files changed
+33
-33
No files matched your search
+3
-3
@@ -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)
|
||||
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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' }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user