At the very first position of a table with no block before it, Enter inserts a paragraph above the table and moves the caret there, as both word processors do — previously the caret could never get before such a table. Generic first-child check, so it also covers a table leading a cell, text box or columns section.
61 lines
2.5 KiB
TypeScript
61 lines
2.5 KiB
TypeScript
import { Extension } from '@tiptap/core';
|
|
import { Plugin, PluginKey } from '@tiptap/pm/state';
|
|
|
|
// Keep an empty paragraph after a trailing table: a table is isolating, so at the very
|
|
// end the cursor gets trapped with nowhere to type below. Appending a paragraph fixes
|
|
// it and self-terminates (once the last child is a paragraph the check no-ops).
|
|
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', 'textBox', 'columns']);
|
|
|
|
export const TrailingNode = Extension.create({
|
|
name: 'trailingNode',
|
|
|
|
// The trap mirrored at the start: nothing can sit before a leading table, so Enter
|
|
// at the table's very first position inserts a paragraph above it and moves the
|
|
// caret there — the only way to get text before such a table.
|
|
addKeyboardShortcuts() {
|
|
return {
|
|
Enter: ({ editor }) => {
|
|
const { $from, empty } = editor.state.selection;
|
|
if (!empty || $from.parentOffset !== 0) return false;
|
|
let depth = 0;
|
|
for (let d = $from.depth; d > 0 && !depth; d--) {
|
|
if ($from.node(d).type.name === 'table') depth = d;
|
|
}
|
|
if (!depth) return false;
|
|
// First row, first cell, first block — anything else is a normal Enter.
|
|
for (let d = depth; d < $from.depth; d++) if ($from.index(d) !== 0) return false;
|
|
const tablePos = $from.before(depth);
|
|
if (editor.state.doc.resolve(tablePos).index() !== 0) return false;
|
|
return editor
|
|
.chain()
|
|
.insertContentAt(tablePos, { type: 'paragraph' })
|
|
.setTextSelection(tablePos + 1)
|
|
.run();
|
|
},
|
|
};
|
|
},
|
|
|
|
addProseMirrorPlugins() {
|
|
return [
|
|
new Plugin({
|
|
key: trailingNodeKey,
|
|
appendTransaction(_transactions, _oldState, newState) {
|
|
// The note section is not body text and sits past it, so the block that needs
|
|
// a paragraph under it is the last one before the section.
|
|
const doc = newState.doc;
|
|
const notes = doc.lastChild?.type.name === 'noteSection' ? doc.lastChild : null;
|
|
const end = doc.content.size - (notes?.nodeSize ?? 0);
|
|
const last = notes ? doc.childBefore(end).node : 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(end, paragraph.create());
|
|
},
|
|
}),
|
|
];
|
|
},
|
|
});
|