CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-prosemirror-view

ProseMirror's view component that manages DOM structure and user interactions for rich text editing

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

editor-props.mddocs/

Editor Props

Editor props provide a comprehensive configuration system for customizing editor behavior, handling events, and integrating with external systems. Props can be provided directly to the EditorView constructor or through plugins, allowing for flexible and modular editor customization.

Capabilities

DirectEditorProps Interface

Props interface for direct editor view creation, extending the base EditorProps.

/**
 * The props object given directly to the editor view supports some
 * fields that can't be used in plugins.
 */
interface DirectEditorProps extends EditorProps {
  /** The current state of the editor */
  state: EditorState;
  
  /** 
   * A set of plugins to use in the view, applying their plugin view
   * and props. Passing plugins with a state component will result
   * in an error, since such plugins must be present in the state.
   */
  plugins?: readonly Plugin[];
  
  /** 
   * The callback over which to send transactions (state updates)
   * produced by the view. If you specify this, you probably want to
   * make sure this ends up calling the view's updateState method
   * with a new state that has the transaction applied.
   */
  dispatchTransaction?(tr: Transaction): void;
}

Usage Examples:

import { EditorView } from "prosemirror-view";
import { EditorState } from "prosemirror-state";

// Basic editor setup
const view = new EditorView(document.querySelector("#editor"), {
  state: EditorState.create({ schema: mySchema }),
  
  dispatchTransaction(tr) {
    // Apply transaction and update state
    const newState = this.state.apply(tr);
    this.updateState(newState);
    
    // Optional: sync with external state management
    if (tr.docChanged) {
      this.props.onDocChange?.(newState.doc);
    }
  },
  
  plugins: [
    // View-specific plugins (no state component)
    myViewPlugin
  ]
});

// React integration example
function ProseMirrorEditor({ initialDoc, onChange }) {
  const [editorState, setEditorState] = useState(
    EditorState.create({ schema: mySchema, doc: initialDoc })
  );
  
  const viewRef = useRef();
  
  useEffect(() => {
    const view = new EditorView(viewRef.current, {
      state: editorState,
      dispatchTransaction(tr) {
        const newState = this.state.apply(tr);
        setEditorState(newState);
        
        if (tr.docChanged && onChange) {
          onChange(newState.doc);
        }
      }
    });
    
    return () => view.destroy();
  }, []);
  
  return <div ref={viewRef} />;
}

Event Handling Props

Props for handling various user interaction events.

interface EditorProps<P = any> {
  /** 
   * Can be an object mapping DOM event type names to functions that
   * handle them. Such functions will be called before any handling
   * ProseMirror does of events fired on the editable DOM element.
   * When returning true from such a function, you are responsible for
   * calling preventDefault yourself.
   */
  handleDOMEvents?: {
    [event in keyof DOMEventMap]?: (
      this: P, 
      view: EditorView, 
      event: DOMEventMap[event]
    ) => boolean | void
  };
  
  /** Called when the editor receives a keydown event */
  handleKeyDown?(this: P, view: EditorView, event: KeyboardEvent): boolean | void;
  
  /** Handler for keypress events */
  handleKeyPress?(this: P, view: EditorView, event: KeyboardEvent): boolean | void;
  
  /** 
   * Whenever the user directly input text, this handler is called
   * before the input is applied. If it returns true, the default
   * behavior of actually inserting the text is suppressed.
   */
  handleTextInput?(
    this: P, 
    view: EditorView, 
    from: number, 
    to: number, 
    text: string, 
    deflt: () => Transaction
  ): boolean | void;
}

Usage Examples:

// Custom keyboard shortcuts
const view = new EditorView(element, {
  state: myState,
  
  handleKeyDown(view, event) {
    // Ctrl+S to save
    if (event.ctrlKey && event.key === "s") {
      saveDocument(view.state.doc);
      event.preventDefault();
      return true;
    }
    
    // Ctrl+B for bold
    if (event.ctrlKey && event.key === "b") {
      const { schema } = view.state;
      const boldMark = schema.marks.strong;
      const tr = view.state.tr.addStoredMark(boldMark.create());
      view.dispatch(tr);
      return true;
    }
    
    return false;
  },
  
  handleTextInput(view, from, to, text, deflt) {
    // Auto-format as user types
    if (text === " " && from > 0) {
      const beforeText = view.state.doc.textBetween(from - 10, from);
      
      // Convert ** to bold
      const boldMatch = beforeText.match(/\*\*(.*?)\*\*$/);
      if (boldMatch) {
        const start = from - boldMatch[0].length;
        const tr = view.state.tr
          .delete(start, from)
          .insertText(boldMatch[1])
          .addMark(start, start + boldMatch[1].length, 
                   view.state.schema.marks.strong.create());
        view.dispatch(tr);
        return true;
      }
    }
    
    return false;
  },
  
  handleDOMEvents: {
    // Custom paste handling
    paste(view, event) {
      const clipboardData = event.clipboardData;
      const items = clipboardData?.items;
      
      // Handle image paste
      for (let item of items || []) {
        if (item.type.startsWith("image/")) {
          const file = item.getAsFile();
          if (file) {
            handleImagePaste(view, file);
            event.preventDefault();
            return true;
          }
        }
      }
      
      return false;
    },
    
    // Custom drag and drop
    drop(view, event) {
      const files = event.dataTransfer?.files;
      if (files && files.length > 0) {
        const file = files[0];
        if (file.type.startsWith("image/")) {
          const coords = view.posAtCoords({
            left: event.clientX,
            top: event.clientY
          });
          
          if (coords) {
            handleImageDrop(view, file, coords.pos);
            event.preventDefault();
            return true;
          }
        }
      }
      
      return false;
    }
  }
});

Click and Mouse Event Props

Props for handling mouse interactions and clicks.

interface EditorProps<P = any> {
  /** 
   * Called for each node around a click, from the inside out.
   * The `direct` flag will be true for the inner node.
   */
  handleClickOn?(
    this: P, 
    view: EditorView, 
    pos: number, 
    node: Node, 
    nodePos: number, 
    event: MouseEvent, 
    direct: boolean
  ): boolean | void;
  
  /** Called when the editor is clicked, after handleClickOn handlers */
  handleClick?(this: P, view: EditorView, pos: number, event: MouseEvent): boolean | void;
  
  /** Called for each node around a double click */
  handleDoubleClickOn?(
    this: P, 
    view: EditorView, 
    pos: number, 
    node: Node, 
    nodePos: number, 
    event: MouseEvent, 
    direct: boolean
  ): boolean | void;
  
  /** Called when the editor is double-clicked, after handleDoubleClickOn */
  handleDoubleClick?(this: P, view: EditorView, pos: number, event: MouseEvent): boolean | void;
  
  /** Called for each node around a triple click */
  handleTripleClickOn?(
    this: P, 
    view: EditorView, 
    pos: number, 
    node: Node, 
    nodePos: number, 
    event: MouseEvent, 
    direct: boolean
  ): boolean | void;
  
  /** Called when the editor is triple-clicked, after handleTripleClickOn */
  handleTripleClick?(this: P, view: EditorView, pos: number, event: MouseEvent): boolean | void;
}

Usage Examples:

// Interactive editor with click handlers
const view = new EditorView(element, {
  state: myState,
  
  handleClickOn(view, pos, node, nodePos, event, direct) {
    // Handle image clicks
    if (node.type.name === "image") {
      openImageEditor(node, nodePos);
      return true;
    }
    
    // Handle link clicks with modifier
    if (node.marks.find(mark => mark.type.name === "link") && event.ctrlKey) {
      const linkMark = node.marks.find(mark => mark.type.name === "link");
      if (linkMark) {
        window.open(linkMark.attrs.href, "_blank");
        return true;
      }
    }
    
    return false;
  },
  
  handleClick(view, pos, event) {
    // Show context menu on right click
    if (event.button === 2) {
      showContextMenu(event.clientX, event.clientY, view, pos);
      event.preventDefault();
      return true;
    }
    
    return false;
  },
  
  handleDoubleClickOn(view, pos, node, nodePos, event, direct) {
    // Double-click to edit text nodes inline
    if (node.isText && direct) {
      startInlineEdit(view, nodePos, node);
      return true;
    }
    
    return false;
  },
  
  handleTripleClick(view, pos, event) {
    // Triple-click to select entire paragraph
    const $pos = view.state.doc.resolve(pos);
    const start = $pos.start($pos.depth);
    const end = $pos.end($pos.depth);
    
    const selection = TextSelection.create(view.state.doc, start, end);
    view.dispatch(view.state.tr.setSelection(selection));
    
    return true;
  }
});

Content Processing Props

Props for transforming content during paste, drop, and copy operations.

interface EditorProps<P = any> {
  /** Can be used to override the behavior of pasting */
  handlePaste?(this: P, view: EditorView, event: ClipboardEvent, slice: Slice): boolean | void;
  
  /** 
   * Called when something is dropped on the editor. `moved` will be
   * true if this drop moves from the current selection.
   */
  handleDrop?(
    this: P, 
    view: EditorView, 
    event: DragEvent, 
    slice: Slice, 
    moved: boolean
  ): boolean | void;
  
  /** Can be used to transform pasted HTML text, before it is parsed */
  transformPastedHTML?(this: P, html: string, view: EditorView): string;
  
  /** Transform pasted plain text. The `plain` flag will be true when
   * the text is pasted as plain text. */
  transformPastedText?(this: P, text: string, plain: boolean, view: EditorView): string;
  
  /** 
   * Can be used to transform pasted or dragged-and-dropped content
   * before it is applied to the document.
   */
  transformPasted?(this: P, slice: Slice, view: EditorView): Slice;
  
  /** 
   * Can be used to transform copied or cut content before it is
   * serialized to the clipboard.
   */
  transformCopied?(this: P, slice: Slice, view: EditorView): Slice;
}

Usage Examples:

// Advanced paste handling
const view = new EditorView(element, {
  state: myState,
  
  handlePaste(view, event, slice) {
    // Custom handling for specific content types
    const html = event.clipboardData?.getData("text/html");
    
    if (html && html.includes("data-source='external-app'")) {
      const customSlice = parseExternalAppContent(html);
      const tr = view.state.tr.replaceSelection(customSlice);
      view.dispatch(tr);
      return true;
    }
    
    return false;
  },
  
  transformPastedHTML(html, view) {
    // Clean up pasted HTML
    return html
      .replace(/<script[^>]*>.*?<\/script>/gi, '') // Remove scripts
      .replace(/style="[^"]*"/gi, '') // Remove inline styles
      .replace(/<(div|span)([^>]*)>/gi, '<p$2>') // Convert divs/spans to paragraphs
      .replace(/<\/(div|span)>/gi, '</p>');
  },
  
  transformPastedText(text, plain, view) {
    if (!plain) {
      // Auto-link URLs in rich text paste
      return text.replace(
        /(https?:\/\/[^\s]+)/g,
        '<a href="$1">$1</a>'
      );
    }
    
    // Clean plain text
    return text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
  },
  
  transformPasted(slice, view) {
    // Transform pasted content
    let transformedSlice = slice;
    
    // Convert external links to internal format
    transformedSlice = transformExternalLinks(transformedSlice);
    
    // Apply custom formatting rules
    transformedSlice = applyFormattingRules(transformedSlice);
    
    return transformedSlice;
  },
  
  transformCopied(slice, view) {
    // Add metadata when copying
    return addCopyMetadata(slice, {
      source: "my-editor",
      timestamp: Date.now(),
      user: getCurrentUser()
    });
  }
});

Parsing and Serialization Props

Props for customizing how content is parsed and serialized.

interface EditorProps<P = any> {
  /** 
   * The DOMParser to use when reading editor changes from the DOM.
   * Defaults to calling DOMParser.fromSchema on the editor's schema.
   */
  domParser?: DOMParser;
  
  /** 
   * The DOMParser to use when reading content from the clipboard.
   * When not given, the value of the domParser prop is used.
   */
  clipboardParser?: DOMParser;
  
  /** 
   * A function to parse text from the clipboard into a document slice.
   * Called after transformPastedText. The `plain` flag will be true
   * when the text is pasted as plain text.
   */
  clipboardTextParser?(
    this: P, 
    text: string, 
    $context: ResolvedPos, 
    plain: boolean, 
    view: EditorView
  ): Slice;
  
  /** 
   * The DOM serializer to use when putting content onto the clipboard.
   * If not given, the result of DOMSerializer.fromSchema will be used.
   */
  clipboardSerializer?: DOMSerializer;
  
  /** 
   * A function that will be called to get the text for the current
   * selection when copying text to the clipboard.
   */
  clipboardTextSerializer?(this: P, content: Slice, view: EditorView): string;
}

Usage Examples:

import { DOMParser, DOMSerializer } from "prosemirror-model";

// Custom parsing and serialization
const customDOMParser = DOMParser.fromSchema(schema).extend({
  // Custom parsing rules
  parseHTML: (html) => {
    // Pre-process HTML before parsing
    const cleanedHTML = sanitizeHTML(html);
    return DOMParser.fromSchema(schema).parseHTML(cleanedHTML);
  }
});

const view = new EditorView(element, {
  state: myState,
  
  domParser: customDOMParser,
  
  clipboardTextParser(text, $context, plain, view) {
    if (plain) {
      // Custom plain text parsing
      const lines = text.split('\n');
      const content = lines.map(line => {
        if (line.startsWith('# ')) {
          return schema.nodes.heading.create(
            { level: 1 }, 
            schema.text(line.slice(2))
          );
        } else if (line.startsWith('- ')) {
          return schema.nodes.list_item.create(
            null,
            schema.nodes.paragraph.create(null, schema.text(line.slice(2)))
          );
        } else {
          return schema.nodes.paragraph.create(null, schema.text(line));
        }
      });
      
      return new Slice(Fragment.from(content), 0, 0);
    }
    
    // Use default parsing for rich text
    return null;
  },
  
  clipboardTextSerializer(content, view) {
    // Custom text serialization for copying
    let text = "";
    
    content.content.forEach(node => {
      if (node.type.name === "heading") {
        text += "#".repeat(node.attrs.level) + " " + node.textContent + "\n";
      } else if (node.type.name === "list_item") {
        text += "- " + node.textContent + "\n";
      } else {
        text += node.textContent + "\n";
      }
    });
    
    return text;
  }
});

Rendering and Behavior Props

Props for customizing editor rendering and behavior.

interface EditorProps<P = any> {
  /** 
   * Allows you to pass custom rendering and behavior logic for nodes.
   * Should map node names to constructor functions that produce a
   * NodeView object implementing the node's display behavior.
   */
  nodeViews?: {[node: string]: NodeViewConstructor};
  
  /** 
   * Pass custom mark rendering functions. Note that these cannot
   * provide the kind of dynamic behavior that node views can.
   */
  markViews?: {[mark: string]: MarkViewConstructor};
  
  /** A set of document decorations to show in the view */
  decorations?(this: P, state: EditorState): DecorationSource | null | undefined;
  
  /** When this returns false, the content of the view is not directly editable */
  editable?(this: P, state: EditorState): boolean;
  
  /** 
   * Control the DOM attributes of the editable element. May be either
   * an object or a function going from an editor state to an object.
   */
  attributes?: 
    | {[name: string]: string} 
    | ((state: EditorState) => {[name: string]: string});
}

Usage Examples:

// Complete editor setup with all props
const view = new EditorView(element, {
  state: myState,
  
  nodeViews: {
    image: (node, view, getPos) => new CustomImageView(node, view, getPos),
    code_block: (node, view, getPos) => new CodeBlockView(node, view, getPos)
  },
  
  markViews: {
    highlight: (mark, view, inline) => new HighlightMarkView(mark, view, inline)
  },
  
  decorations(state) {
    // Add decorations based on current state
    const decorations = [];
    
    // Highlight search results
    if (this.searchTerm) {
      const searchMatches = findSearchMatches(state.doc, this.searchTerm);
      searchMatches.forEach(match => {
        decorations.push(
          Decoration.inline(match.from, match.to, {
            class: "search-highlight"
          })
        );
      });
    }
    
    // Add spell check decorations
    const spellErrors = runSpellCheck(state.doc);
    spellErrors.forEach(error => {
      decorations.push(
        Decoration.inline(error.from, error.to, {
          class: "spell-error",
          title: `Suggestion: ${error.suggestions.join(", ")}`
        })
      );
    });
    
    return DecorationSet.create(state.doc, decorations);
  },
  
  editable(state) {
    // Make editor read-only in certain conditions
    return !state.doc.firstChild?.attrs.readOnly;
  },
  
  attributes(state) {
    return {
      class: "editor-content",
      "data-editor-mode": state.doc.attrs.mode || "normal",
      spellcheck: "false",
      autocorrect: "off",
      autocapitalize: "off"
    };
  }
});

Install with Tessl CLI

npx tessl i tessl/npm-prosemirror-view

docs

core-editor-view.md

custom-views.md

decoration-system.md

editor-props.md

index.md

input-handling.md

position-mapping.md

tile.json