Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/editor-final-paragraph.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@office-kit/docx-editor": patch
---

The editor keeps a paragraph after a table that ends the document, as Word does. Before, there was no way to type below such a table. This affected documents that end with a table, and tables inserted at the end of a document.
21 changes: 19 additions & 2 deletions packages/editor/src/history.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
openEditor,
runCommand,
} from "./index.js";
import { caretAt } from "./selection.js";

function editorWith(texts: string[]): EditorModel {
const model = editorFor(createDocx({ paragraphs: texts }));
Expand Down Expand Up @@ -141,20 +142,36 @@ describe("replaceAllCommand", () => {
expect(model.canUndo()).toBe(false);
});

it("keeps a paragraph after a table that ends the document", () => {
const doc = createDocx({ paragraphs: ["intro"] });
addTable(doc, [["cell"]]);
const model = editorFor(doc);
expect(model.doc.document.body.blocks.map((b) => b.kind)).toEqual([
"paragraph",
"table",
"paragraph",
]);
// Inserting a table at the end leaves the caret's paragraph after it, too.
model.setSelection(caretAt({ block: 2, inline: 0, offset: 0 }));
runCommand(model, commands.insertTableCommand, { rows: 1, cols: 1 });
expect(model.doc.document.body.blocks.at(-1)?.kind).toBe("paragraph");
});

it("replaces inside table cells in the same undo step, and redo re-applies it", () => {
const doc = createDocx({ paragraphs: ["cat"] });
addTable(doc, [["cat cell"]]);
// The editor adds the paragraph Word keeps after a table that ends the body.
const model = editorFor(doc);
expect(
runCommand(model, commands.replaceAllCommand, { query: "cat", replacement: "dog" }),
).toBe(2);
const cell = (): string => getTableCellText(tables(model.doc)[0]!, 0, 0);
expect(cell()).toBe("dog cell");
model.undo();
expect([texts(model), cell()]).toEqual([["cat"], "cat cell"]);
expect([texts(model), cell()]).toEqual([["cat", ""], "cat cell"]);
expect(model.canUndo()).toBe(false);
model.redo();
expect([texts(model), cell()]).toEqual([["dog"], "dog cell"]);
expect([texts(model), cell()]).toEqual([["dog", ""], "dog cell"]);
const reopened = openDocx(toUint8Array(model.doc));
expect(getTableCellText(tables(reopened)[0]!, 0, 0)).toBe("dog cell");
});
Expand Down
13 changes: 12 additions & 1 deletion packages/editor/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* do one thing").
*/

import { clone, type Docx, storyView } from "@office-kit/docx";
import { appendParagraph, clone, type Docx, storyView } from "@office-kit/docx";
import type { DocPosition, Selection, StoryRef } from "./selection.js";

export interface EditorSnapshot {
Expand All @@ -19,6 +19,15 @@ export interface EditorSnapshot {

export type ChangeListener = (model: EditorModel) => void;

/**
* Word's body always ends with a paragraph: a document that ends with a table
* (or is empty) gets one on open, and an edit that leaves a table last gets
* one too. Without it there is nowhere to put the caret below the table.
*/
function ensureFinalParagraph(doc: Docx): void {
if (doc.document.body.blocks.at(-1)?.kind !== "paragraph") appendParagraph(doc, "");
}

/** How deep the undo/redo history is allowed to grow. */
const DEFAULT_HISTORY_LIMIT = 200;

Expand All @@ -42,6 +51,7 @@ export class EditorModel {
constructor(doc: Docx, options: { historyLimit?: number } = {}) {
this.docState = doc;
this.historyLimit = options.historyLimit ?? DEFAULT_HISTORY_LIMIT;
ensureFinalParagraph(doc);
}

/**
Expand Down Expand Up @@ -113,6 +123,7 @@ export class EditorModel {
this.pending = null;
if (nextSelection !== undefined) this.setSelectionQuietly(nextSelection);
this.editStory = null;
ensureFinalParagraph(this.docState);
this.docState.dirty = true;
this.emit();
}
Expand Down
3 changes: 2 additions & 1 deletion packages/editor/src/range-edit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,11 @@ describe("table cells", () => {

it("Enter splits the cell paragraph instead of adding one after the table", () => {
const model = cellModel();
const blocks = model.doc.document.body.blocks.length;
model.setSelection({ anchor: at(0, 3), focus: at(0, 3) });
runCommand(model, commands.splitParagraphCommand, undefined);
expect(cellTexts(model)).toEqual(["abc", "def"]);
expect(model.doc.document.body.blocks).toHaveLength(2);
expect(model.doc.document.body.blocks).toHaveLength(blocks);
expect(model.selection?.focus).toMatchObject({ block: 1, cell: { row: 0, col: 0 }, para: 1 });
expect(validate(openDocx(toUint8Array(model.doc)))).toHaveLength(0);
});
Expand Down
Loading