Skip to content

Gapcursor

Gapcursor allows the cursor to be placed in positions where normal text selection is not possible, such as before or after tables, images, horizontal rules, and other block nodes that don’t contain editable text. Without it, users can get “stuck” with no way to place the cursor next to these elements.

Included in StarterKit by default.

Gapcursor is included in StarterKit, so it works out of the box. To use it standalone:

import { Editor, Document, Paragraph, Text, Gapcursor } from '@domternal/core';
import '@domternal/theme';
const editor = new Editor({
element: document.getElementById('editor')!,
extensions: [Document, Paragraph, Text, Gapcursor],
});

To disable Gapcursor in StarterKit:

StarterKit.configure({ gapcursor: false })

Gapcursor has no configurable options.

Gapcursor does not register any commands.

Gapcursor hooks into the standard arrow keys to enable navigation into gap positions:

KeyDescription
ArrowLeftMove to the previous gap cursor position (if at a text block boundary)
ArrowRightMove to the next gap cursor position (if at a text block boundary)
ArrowUpMove to the gap cursor position above (if at a text block boundary)
ArrowDownMove to the gap cursor position below (if at a text block boundary)

These are not custom shortcuts but handlers on the existing arrow keys. They only activate when the cursor is at the edge of a text block and a valid gap cursor position exists in that direction.

Gapcursor does not register any input rules.

Gapcursor does not register any toolbar items.

A gap cursor is a special selection type (GapCursor) that represents the cursor at a position where no text node exists. It appears as a small blinking horizontal line, similar to a regular text cursor but positioned in the gap between block nodes.

Common gap cursor positions:

  • Before or after a table
  • Before or after an image (when not inline)
  • Before or after a horizontal rule
  • Between two block nodes that don’t contain editable text
  • At the start or end of the document when it begins/ends with a non-text block

A position is valid for a gap cursor when all of these are true:

  1. The parent node is not a text block
  2. There is a “closed” block (atomic, isolating, or gap-creating node) before the position
  3. There is a “closed” block after the position
  4. The content match at that position allows a text block (or allowGapCursor is set)

The gap cursor is rendered as a ProseMirror widget decoration with the CSS class ProseMirror-gapcursor. The @domternal/theme package includes the necessary styles, but if you are building a custom theme, you need:

.ProseMirror-gapcursor {
display: none;
pointer-events: none;
position: absolute;
}
.ProseMirror-gapcursor:after {
content: "";
display: block;
position: absolute;
top: -2px;
width: 20px;
border-top: 1px solid black;
animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;
}
@keyframes ProseMirror-cursor-blink {
to { visibility: hidden; }
}
.ProseMirror-focused .ProseMirror-gapcursor {
display: block;
}

The cursor is hidden by default and only shown when the editor has focus (.ProseMirror-focused).

Clicking on a valid gap cursor position creates a gap cursor there, unless the click target is a selectable node (like an image with NodeSelection).

When composition input starts while a gap cursor is active, the plugin creates temporary inline structure at the gap position so the IME text has a valid place to go. This ensures gap cursor positions work correctly with non-Latin input methods.

Node types can override gap cursor behavior using the allowGapCursor property in their ProseMirror spec:

// Force allow gap cursor before/after this node type
{ allowGapCursor: true }
// Force disallow gap cursor before/after this node type
{ allowGapCursor: false }

When not set, the default validation logic applies.

import { Gapcursor } from '@domternal/core';
ExportTypeDescription
GapcursorExtensionThe gapcursor extension

For low-level access to ProseMirror’s gap cursor utilities:

import { gapCursor, GapCursor } from '@domternal/pm/gapcursor';
ExportTypeDescription
gapCursor() => PluginThe ProseMirror gap cursor plugin factory
GapCursorSelection subclassThe gap cursor selection type, with valid($pos) and findGapCursorFrom($pos, dir) static methods

@domternal/core - Gapcursor.ts