CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-tiptap--core

Headless rich text editor built on ProseMirror with extensible architecture for building custom editors

94

1.00x
Overview
Eval results
Files

command-system.mddocs/

Command System

The command system in @tiptap/core provides a powerful and flexible way to execute editor actions. Commands can be executed individually, chained together for complex operations, or validated before execution.

Capabilities

CommandManager

The CommandManager class handles command execution, chaining, and validation within the editor context.

/**
 * Manages command execution and validation for the editor
 */
class CommandManager {
  /**
   * Create a new CommandManager instance
   * @param props - Configuration including editor and state
   */
  constructor(props: { 
    editor: Editor; 
    state: EditorState 
  });
  
  /** Direct access to individual commands */
  readonly commands: SingleCommands;
  
  /**
   * Create a command chain for sequential execution
   * @returns ChainedCommands instance for chaining operations
   */
  chain(): ChainedCommands;
  
  /**
   * Create command validation interface
   * @returns CanCommands instance for testing executability
   */
  can(): CanCommands;
  
  /**
   * Create a custom command chain with specific transaction and dispatch behavior
   * @param startTr - Optional starting transaction
   * @param shouldDispatch - Whether commands should be dispatched immediately
   * @returns ChainedCommands instance
   */
  createChain(
    startTr?: Transaction, 
    shouldDispatch?: boolean
  ): ChainedCommands;
  
  /**
   * Create a custom command validation instance
   * @param startTr - Optional starting transaction
   * @returns CanCommands instance
   */
  createCan(startTr?: Transaction): CanCommands;
  
  /**
   * Build command properties for command execution
   * @param tr - Transaction to build props for
   * @param shouldDispatch - Whether to include dispatch function
   * @returns CommandProps object
   */
  buildProps(
    tr: Transaction, 
    shouldDispatch?: boolean
  ): CommandProps;
}

Single Commands

Interface for executing individual commands that return boolean success values.

/**
 * Interface for executing individual commands
 */
interface SingleCommands {
  [commandName: string]: (attributes?: Record<string, any>) => boolean;
  
  // Core commands available by default
  insertContent(value: Content, options?: InsertContentOptions): boolean;
  deleteSelection(): boolean;
  deleteRange(range: { from: number; to: number }): boolean;
  enter(): boolean;
  focus(position?: FocusPosition, options?: { scrollIntoView?: boolean }): boolean;
  blur(): boolean;
  selectAll(): boolean;
  selectTextblockStart(): boolean;
  selectTextblockEnd(): boolean;
  selectNodeForward(): boolean;
  selectNodeBackward(): boolean;
  selectParentNode(): boolean;
  
  // Mark commands
  setMark(typeOrName: string | MarkType, attributes?: Record<string, any>): boolean;
  toggleMark(typeOrName: string | MarkType, attributes?: Record<string, any>): boolean;
  unsetMark(typeOrName: string | MarkType, options?: { extendEmptyMarkRange?: boolean }): boolean;
  
  // Node commands  
  setNode(typeOrName: string | NodeType, attributes?: Record<string, any>): boolean;
  toggleNode(typeOrName: string | NodeType, toggleTypeOrName?: string | NodeType, attributes?: Record<string, any>): boolean;
  wrapIn(typeOrName: string | NodeType, attributes?: Record<string, any>): boolean;
  lift(typeOrName?: string | NodeType, attributes?: Record<string, any>): boolean;
  liftEmptyBlock(): boolean;
  splitBlock(options?: { keepMarks?: boolean }): boolean;
  joinBackward(): boolean;
  joinForward(): boolean;
  
  // Custom commands added by extensions
  [extensionCommand: string]: (attributes?: Record<string, any>) => boolean;
}

interface InsertContentOptions {
  parseOptions?: ParseOptions;
  updateSelection?: boolean;
}

type Content = 
  | string
  | JSONContent
  | JSONContent[]
  | ProseMirrorNode
  | ProseMirrorNode[]
  | ProseMirrorFragment;

Usage Examples:

// Execute individual commands
const success = editor.commands.insertContent('Hello World!');

if (editor.commands.focus()) {
  editor.commands.selectAll();
}

// Mark commands
editor.commands.setMark('bold');
editor.commands.toggleMark('italic');
editor.commands.unsetMark('link');

// Node commands
editor.commands.setNode('heading', { level: 1 });
editor.commands.wrapIn('blockquote');
editor.commands.lift('listItem');

// Insert complex content
editor.commands.insertContent({
  type: 'paragraph',
  content: [
    {
      type: 'text',
      text: 'Bold text',
      marks: [{ type: 'bold' }]
    }
  ]
});

// Delete and select commands
editor.commands.deleteSelection();
editor.commands.selectTextblockStart();
editor.commands.selectParentNode();

Chained Commands

Interface for chaining multiple commands together for sequential execution.

/**
 * Interface for chaining commands together
 */
interface ChainedCommands {
  [commandName: string]: (attributes?: Record<string, any>) => ChainedCommands;
  
  /**
   * Execute the entire command chain
   * @returns Whether all commands in the chain succeeded
   */
  run(): boolean;
  
  // All single commands are available for chaining
  insertContent(value: Content, options?: InsertContentOptions): ChainedCommands;
  deleteSelection(): ChainedCommands;
  deleteRange(range: { from: number; to: number }): ChainedCommands;
  enter(): ChainedCommands;
  focus(position?: FocusPosition, options?: { scrollIntoView?: boolean }): ChainedCommands;
  blur(): ChainedCommands;
  selectAll(): ChainedCommands;
  
  setMark(typeOrName: string | MarkType, attributes?: Record<string, any>): ChainedCommands;
  toggleMark(typeOrName: string | MarkType, attributes?: Record<string, any>): ChainedCommands;
  unsetMark(typeOrName: string | MarkType, options?: { extendEmptyMarkRange?: boolean }): ChainedCommands;
  
  setNode(typeOrName: string | NodeType, attributes?: Record<string, any>): ChainedCommands;
  toggleNode(typeOrName: string | NodeType, toggleTypeOrName?: string | NodeType, attributes?: Record<string, any>): ChainedCommands;
  wrapIn(typeOrName: string | NodeType, attributes?: Record<string, any>): ChainedCommands;
  lift(typeOrName?: string | NodeType, attributes?: Record<string, any>): ChainedCommands;
  
  // Custom commands added by extensions
  [extensionCommand: string]: (attributes?: Record<string, any>) => ChainedCommands;
}

Usage Examples:

// Basic command chaining
editor
  .chain()
  .focus()
  .selectAll()
  .deleteSelection()
  .insertContent('New content')
  .run();

// Complex formatting chain
editor
  .chain()
  .focus()
  .toggleMark('bold')
  .toggleMark('italic')
  .insertContent('Bold and italic text')
  .setMark('link', { href: 'https://example.com' })
  .insertContent(' with a link')
  .run();

// Node manipulation chain
editor
  .chain()
  .focus()
  .selectAll()
  .wrapIn('blockquote')
  .setNode('heading', { level: 2 })
  .insertContent('Quoted heading')
  .run();

// Conditional chaining
const success = editor
  .chain()
  .focus()
  .deleteSelection() // Only if something is selected
  .insertContent('Replacement text')
  .run();

if (success) {
  console.log('Chain executed successfully');
}

// Chain with custom commands (from extensions)
editor
  .chain()
  .focus()
  .setFontSize(16)
  .setTextAlign('center')
  .insertTable({ rows: 3, cols: 3 })
  .run();

Command Validation

Interface for testing whether commands can be executed without actually running them.

/**
 * Interface for validating command executability
 */
interface CanCommands {
  [commandName: string]: (attributes?: Record<string, any>) => boolean;
  
  // Core command validation
  insertContent(value: Content, options?: InsertContentOptions): boolean;
  deleteSelection(): boolean;
  deleteRange(range: { from: number; to: number }): boolean;
  enter(): boolean;
  focus(position?: FocusPosition): boolean;
  blur(): boolean;
  selectAll(): boolean;
  
  setMark(typeOrName: string | MarkType, attributes?: Record<string, any>): boolean;
  toggleMark(typeOrName: string | MarkType, attributes?: Record<string, any>): boolean;
  unsetMark(typeOrName: string | MarkType): boolean;
  
  setNode(typeOrName: string | NodeType, attributes?: Record<string, any>): boolean;
  toggleNode(typeOrName: string | NodeType, toggleTypeOrName?: string | NodeType): boolean;
  wrapIn(typeOrName: string | NodeType, attributes?: Record<string, any>): boolean;
  lift(typeOrName?: string | NodeType): boolean;
  
  // Custom commands added by extensions
  [extensionCommand: string]: (attributes?: Record<string, any>) => boolean;
}

Usage Examples:

// Check if commands can be executed
if (editor.can().toggleMark('bold')) {
  editor.commands.toggleMark('bold');
}

if (editor.can().wrapIn('blockquote')) {
  editor.commands.wrapIn('blockquote');
}

// Conditional UI updates
function BoldButton() {
  const canToggleBold = editor.can().toggleMark('bold');
  const isBold = editor.isActive('bold');
  
  return (
    <button 
      disabled={!canToggleBold}
      className={isBold ? 'active' : ''}
      onClick={() => editor.commands.toggleMark('bold')}
    >
      Bold
    </button>
  );
}

// Check complex operations
const canInsertTable = editor.can().insertTable?.({ rows: 3, cols: 3 });
const canSetHeading = editor.can().setNode('heading', { level: 1 });

// Multiple validations
const formattingActions = [
  { name: 'bold', can: editor.can().toggleMark('bold') },
  { name: 'italic', can: editor.can().toggleMark('italic') },
  { name: 'code', can: editor.can().toggleMark('code') },
  { name: 'link', can: editor.can().setMark('link', { href: '' }) }
];

const availableActions = formattingActions.filter(action => action.can);

Command Properties

The properties passed to command functions when they are executed.

/**
 * Properties passed to command functions during execution
 */
interface CommandProps {
  /** The editor instance */
  editor: Editor;
  
  /** Current transaction (may be modified by commands) */
  tr: Transaction;
  
  /** Access to all single commands */
  commands: SingleCommands;
  
  /** Access to command validation */
  can: CanCommands;
  
  /** Create a new command chain */
  chain: () => ChainedCommands;
  
  /** Current editor state */
  state: EditorState;
  
  /** ProseMirror editor view */
  view: EditorView;
  
  /** Dispatch function (undefined in dry-run mode) */
  dispatch: ((tr: Transaction) => void) | undefined;
}

/**
 * Command function signature
 */
type CommandFunction = (props: CommandProps) => boolean;

Creating Custom Commands

How to create custom commands in extensions.

/**
 * Commands configuration for extensions
 */
interface Commands {
  [commandName: string]: (...args: any[]) => CommandFunction;
}

Usage Examples:

import { Extension } from '@tiptap/core';

// Extension with custom commands
const CustomCommands = Extension.create({
  name: 'customCommands',
  
  addCommands() {
    return {
      // Simple command
      insertDate: () => ({ commands }) => {
        const date = new Date().toLocaleDateString();
        return commands.insertContent(date);
      },
      
      // Command with parameters
      insertHeading: (level: number, text: string) => ({ commands, chain }) => {
        return chain()
          .setNode('heading', { level })
          .insertContent(text)
          .run();
      },
      
      // Complex command using transaction
      duplicateLine: () => ({ tr, state, dispatch }) => {
        const { from, to } = state.selection;
        const line = state.doc.textBetween(from, to);
        
        if (!line) return false;
        
        tr.insertText(`\n${line}`, to);
        
        if (dispatch) {
          dispatch(tr);
        }
        
        return true;
      },
      
      // Command that checks state
      toggleHighlight: (color: string = 'yellow') => ({ commands, editor }) => {
        const isActive = editor.isActive('highlight', { color });
        
        if (isActive) {
          return commands.unsetMark('highlight');
        }
        
        return commands.setMark('highlight', { color });
      },
      
      // Command with validation
      wrapInCallout: (type: string = 'info') => ({ commands, can }) => {
        // Only wrap if we can and aren't already in a callout
        if (!can().wrapIn('callout') || editor.isActive('callout')) {
          return false;
        }
        
        return commands.wrapIn('callout', { type });
      }
    };
  }
});

// Using custom commands
editor.commands.insertDate();
editor.commands.insertHeading(1, 'Chapter Title');
editor.commands.duplicateLine();

// Chain custom commands
editor
  .chain()
  .focus()
  .insertHeading(2, 'Section')
  .insertDate()
  .toggleHighlight('blue')
  .run();

// Validate custom commands
if (editor.can().wrapInCallout('warning')) {
  editor.commands.wrapInCallout('warning');
}

Command Execution Flow

Understanding how commands are processed and executed.

/**
 * Command execution involves several phases:
 * 1. Command function is called with CommandProps
 * 2. Command modifies the transaction (tr)
 * 3. Command returns boolean success value
 * 4. Transaction is dispatched (if dispatch is provided)
 * 5. Editor state is updated
 */

// Example command implementation
const exampleCommand = (text: string) => ({ tr, dispatch, state }) => {
  // 1. Validate the command can run
  if (!text || text.trim().length === 0) {
    return false;
  }
  
  // 2. Modify the transaction
  const { from } = state.selection;
  tr.insertText(text, from);
  
  // 3. Dispatch if available (not in validation mode)
  if (dispatch) {
    dispatch(tr);
  }
  
  // 4. Return success
  return true;
};

// Command chaining execution
// Each command in a chain operates on the same transaction
// The transaction is only dispatched when .run() is called
editor
  .chain()
  .command1() // Modifies tr
  .command2() // Modifies same tr
  .command3() // Modifies same tr
  .run();     // Dispatches tr with all modifications

Install with Tessl CLI

npx tessl i tessl/npm-tiptap--core

docs

command-system.md

document-helpers.md

editor-core.md

extension-system.md

index.md

rule-systems.md

utilities.md

tile.json