diff --git a/src/lib/editor/extensions.ts b/src/lib/editor/extensions.ts index 0abd568..2d73d7d 100644 --- a/src/lib/editor/extensions.ts +++ b/src/lib/editor/extensions.ts @@ -27,6 +27,7 @@ import { FormattingMarks } from './formattingMarks'; import { TableView } from './tableView'; import { TableColumnResize } from './tableColumnResize'; import { TableRowResize } from './tableRowResize'; +import { TrailingNode } from './trailingNode'; export const extensions = [ Document, @@ -62,6 +63,9 @@ export const extensions = [ TableCell, TableColumnResize, TableRowResize, + // Guarantees a paragraph after a trailing table so the cursor isn't trapped + // and the user can keep writing below a table at the document's end. + TrailingNode, History, Placeholder.configure({ placeholder: 'Start typing…' }), TextAlign.configure({ diff --git a/src/lib/editor/trailingNode.ts b/src/lib/editor/trailingNode.ts new file mode 100644 index 0000000..a0b41dd --- /dev/null +++ b/src/lib/editor/trailingNode.ts @@ -0,0 +1,32 @@ +import { Extension } from '@tiptap/core'; +import { Plugin, PluginKey } from '@tiptap/pm/state'; + +// Keep an empty paragraph at the end of the document when the last block is a +// table. A table is an isolating block, so if it sits at the very end there is +// nowhere to click or type below it — the cursor gets trapped inside the table +// and the user can't continue the document. When the doc ends with a table we +// append a paragraph; once it's there the last child is a paragraph, so the +// check no-ops on the next round (no loop). +const trailingNodeKey = new PluginKey('trailingNode'); + +// Block types that can't sit at the document end without trapping the cursor. +const NEEDS_TRAILING_AFTER = new Set(['table']); + +export const TrailingNode = Extension.create({ + name: 'trailingNode', + + addProseMirrorPlugins() { + return [ + new Plugin({ + key: trailingNodeKey, + appendTransaction(_transactions, _oldState, newState) { + const last = newState.doc.lastChild; + if (!last || !NEEDS_TRAILING_AFTER.has(last.type.name)) return null; + const paragraph = newState.schema.nodes.paragraph; + if (!paragraph) return null; + return newState.tr.insert(newState.doc.content.size, paragraph.create()); + }, + }), + ]; + }, +});