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],});import { Component, signal } from '@angular/core';import { DomternalEditorComponent } from '@domternal/angular';import { Editor, Document, Paragraph, Text, Gapcursor } from '@domternal/core';
@Component({ selector: 'app-editor', imports: [DomternalEditorComponent], templateUrl: './editor.html',})export class EditorComponent { editor = signal<Editor | null>(null); extensions = [Document, Paragraph, Text, Gapcursor];}<domternal-editor [extensions]="extensions" (editorCreated)="editor.set($event)"/>import { Domternal } from '@domternal/react';import { Document, Paragraph, Text, Gapcursor } from '@domternal/core';
export default function Editor() { return ( <Domternal extensions={[Document, Paragraph, Text, Gapcursor]} > <Domternal.Content /> </Domternal> );}import { Editor, Document, Paragraph, Text, Gapcursor } from '@domternal/core';
const editor = new Editor({ element: document.getElementById('editor')!, extensions: [Document, Paragraph, Text, Gapcursor],});To disable Gapcursor in StarterKit:
StarterKit.configure({ gapcursor: false })Options
Section titled “Options”Gapcursor has no configurable options.
Commands
Section titled “Commands”Gapcursor does not register any commands.
Keyboard shortcuts
Section titled “Keyboard shortcuts”Gapcursor hooks into the standard arrow keys to enable navigation into gap positions:
| Key | Description |
|---|---|
ArrowLeft | Move to the previous gap cursor position (if at a text block boundary) |
ArrowRight | Move to the next gap cursor position (if at a text block boundary) |
ArrowUp | Move to the gap cursor position above (if at a text block boundary) |
ArrowDown | Move 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.
Input rules
Section titled “Input rules”Gapcursor does not register any input rules.
Toolbar items
Section titled “Toolbar items”Gapcursor does not register any toolbar items.
How it works
Section titled “How it works”What is a gap cursor?
Section titled “What is a gap cursor?”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
When is a position valid?
Section titled “When is a position valid?”A position is valid for a gap cursor when all of these are true:
- The parent node is not a text block
- There is a “closed” block (atomic, isolating, or gap-creating node) before the position
- There is a “closed” block after the position
- The content match at that position allows a text block (or
allowGapCursoris set)
Rendering
Section titled “Rendering”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).
Click handling
Section titled “Click handling”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).
Composition (IME) input
Section titled “Composition (IME) input”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.
Controlling per node type
Section titled “Controlling per node type”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.
Exports
Section titled “Exports”import { Gapcursor } from '@domternal/core';| Export | Type | Description |
|---|---|---|
Gapcursor | Extension | The gapcursor extension |
For low-level access to ProseMirror’s gap cursor utilities:
import { gapCursor, GapCursor } from '@domternal/pm/gapcursor';| Export | Type | Description |
|---|---|---|
gapCursor | () => Plugin | The ProseMirror gap cursor plugin factory |
GapCursor | Selection subclass | The gap cursor selection type, with valid($pos) and findGapCursorFrom($pos, dir) static methods |
Source
Section titled “Source”@domternal/core - Gapcursor.ts