AZ.dev

Insert text at the selection in ProseMirror without breaking select-all or image selections

tiptap: 3 prosemirror-state: 1.4 prosemirror-history: 1.5

To put text where the user’s selection is from code (dictation, snippets, autocomplete), call tr.insertText(text) with no positions, and give block selections a textblock first.

import { NodeSelection, TextSelection, type EditorState, type Transaction } from "prosemirror-state";
import { GapCursor } from "prosemirror-gapcursor";
import { closeHistory } from "prosemirror-history";

export function insertAtSelection(state: EditorState, text: string): Transaction | null {
  const tr = state.tr;
  const selection = state.selection;

  // A selected image or a gap cursor has no textblock to type into.
  // Add one after it, so the image isn't replaced.
  if ((selection instanceof NodeSelection && !selection.node.isInline) || selection instanceof GapCursor) {
    const $pos = state.doc.resolve(selection.to);
    const block = $pos.parent.contentMatchAt($pos.index()).defaultType?.createAndFill();
    if (!block?.isTextblock) return null;
    tr.insert(selection.to, block);
    tr.setSelection(TextSelection.create(tr.doc, selection.to + 1));
  }

  tr.insertText(text); // no from/to: replace the selection itself
  return closeHistory(tr).scrollIntoView();
}

// Dispatch, then close the undo step behind the insert as well.
const tr = insertAtSelection(view.state, "hello");
if (tr) {
  view.dispatch(tr);
  view.dispatch(closeHistory(view.state.tr));
}

In TipTap, import the same names from @tiptap/pm/state, @tiptap/pm/gapcursor and @tiptap/pm/history, and use editor.view.

Why positions break it

tr.insertText(text, from, to) replaces the range, then keeps the old selection mapped through the change. It only collapses the selection when the selection ends exactly where the new text ends. So with from = selection.from, to = selection.to:

With no positions, insertText calls replaceSelectionWith. That hands the text to the selection’s own replaceWith, so each selection type (cells included) handles itself. It takes marks the way typing does (stored marks, else the marks at the cursor, else the marks across the range), and it moves the caret to the end of the new text.

Gotchas

prosemirror tiptap transactions selection undo editor

Was this helpful?

Related Entries

Array reduce: sum values and transform objects