ProseMirror Documentation and Demos

repository·master·Indexed 42 days ago

https://github.com/prosemirror/website

Source code and documentation for the ProseMirror website (prosemirror.net). Includes reference guides and interactive demos for implementing a basic editor, integrating CodeMirror as a NodeView, creating custom node specifications, and implementing advanced features like section folding and custom menu items using prosemirror-state, prosemirror-view, and prosemirror-model.

Tokens
39.3K
Snippets
103
Records
144
Agent score
51%

What's inside ProseMirror

  1. What is a ProseMirror command and how to implement one

    master

    In ProseMirror, a command is a function that implements an editing action (e.g., triggered by a keypress or menu interaction).

    Interface

    A command typically follows this signature: function(state, dispatch, view): boolean

    • state: The current EditorState.
    • dispatch: An optional function that takes a transaction (e.g., view.dispatch). If dispatch is not provided (passed as null), the command should only check if it is applicable and return true or false without performing any action. This allows you to query command applicability (e.g., for graying out menu items) without executing them.
    • view: An optional third argument representing the EditorView, used if the command needs to interact with the DOM.

    Implementation Rules

    1. Applicability: If the command cannot be performed in the current state, return false and do nothing.
    2. Execution: If the command is applicable, dispatch a transaction and return true.
    3. Side Effects: While most commands dispatch transactions, they can also perform side effects like opening dialogs.

    To check if a command is applicable: command(view.state, null) To execute a command: command(view.state, view.dispatch)

    // A standard command implementation
    function deleteSelection(state, dispatch) {
      if (state.selection.empty) return false
      if (dispatch) dispatch(state.tr.deleteSelection())
      return true
    }
    
    // Example of a command using the view for side effects
    function blinkView(_state, dispatch, view) {
      if (dispatch) {
        view.dom.style.background = "yellow"
        setTimeout(() => view.dom.style.background = "", 1000)
      }
      return true
    }
  2. Update node attributes using getPos and setNodeMarkup

    master

    When creating a Node view, the factory function provides a getPos getter. This function can be used to find the current position of the node in the document. To change the node's attributes (like an image's src or alt text) in response to a UI event, use view.dispatch with a transaction created via view.state.tr.setNodeMarkup(pos, null, attributes).

    setNodeMarkup updates the type or the set of attributes for the node at the specified position.

    // Inside a Node view event handler
    let alt = prompt("New alt text:", "")
    if (alt) view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, {
      src: node.attrs.src,
      alt
    }))
  3. Include custom elements in a ProseMirror document

    master

    To include site-specific elements (like domain-specific widgets or custom objects) in a ProseMirror document, you must extend the document schema. This involves two main steps:

    1. Define a NodeSpec: Describe the behavior and DOM representation of your custom node using NodeSpec.
    2. Create a Schema: Create a new schema instance that includes your custom node specification alongside existing nodes (e.g., extending prosemirror-schema-basic).

    Once defined, these custom elements can be parsed from HTML and treated as proper semantic elements within the ProseMirror document.

  4. What makes up the ProseMirror state?

    master

    The EditorState is an immutable object that represents the entire state of an editor at a single point in time. It consists of three core components:

    1. doc: The current document structure.
    2. selection: The current user selection (e.g., a cursor or highlighted text).
    3. storedMarks: A set of marks (like bold or italic) that are active but haven't been applied to the document yet.

    Additionally, the state stores the active set of plugins, which may define their own custom state slots (e.g., for undo history).

    import {schema} from "prosemirror-schema-basic"
    import {EditorState} from "prosemirror-state"
    
    let state = EditorState.create({schema})
    console.log(state.doc.toString()) // An empty paragraph
    console.log(state.selection.from) // 1, the start of the paragraph
  5. Understand the ProseMirror EditorView

    master

    An EditorView is the user interface component that displays an EditorState and handles direct user interactions like typing, clicking, copying, pasting, and dragging.

    Note that the core EditorView has a narrow responsibility. Higher-level features like menus or complex key bindings are typically implemented via plugins rather than being part of the core view component.

  6. Manage node content with contentDOM

    master

    Node views handle content in two ways:

    1. Managed Content: By providing a contentDOM property on your Node view instance, you tell ProseMirror which DOM element should host the node's children. ProseMirror will then handle rendering and updating the content within that element.
    2. Unmanaged Content: If you do not provide a contentDOM, the node's content becomes a "black box" to the editor. You are responsible for rendering the content and handling all user interactions with it.

    For example, in a paragraph where text should be rendered directly into the paragraph tag itself, you would set this.contentDOM = this.dom.

    class ParagraphView {
      constructor(node) {
        // The content is rendered directly into the <p> element
        this.dom = this.contentDOM = document.createElement("p")
      }
    
      update(node) {
        // Logic to update the DOM based on the new node
        return true
      }
    }
  7. Requirements for a central authority in collaborative editing

    master

    To implement a central authority for ProseMirror collaboration, your system must fulfill three core responsibilities:

    • Track document version: Maintain a current version number for the document.
    • Accept changes: Receive changes from editors, validate them, and if applicable, add them to the official list of changes.
    • Provide change history: Offer a mechanism for editors to retrieve all changes that have occurred since a specific version number.
  8. How to use Plugins and Plugin State

    master

    Plugins are used to extend editor behavior. When creating an EditorState, you provide an array of Plugin instances. Plugins can:

    1. Add props to the editor view (e.g., handleKeyDown) to respond to events.
    2. Define their own private state slots using the state property in the plugin specification.

    To access a plugin's state, use plugin.getState(state). Because the editor state is immutable, plugin state values must also be immutable; the plugin's apply method must return a new value rather than mutating the old one.

    // Example: A plugin that adds event handling
    let myPlugin = new Plugin({
      props: {
        handleKeyDown(view, event) {
          console.log("A key was pressed!")
          return false // We did not handle this
        }
      }
    })
    
    let state = EditorState.create({schema, plugins: [myPlugin]})
    
    // Example: A plugin with its own state
    let transactionCounter = new Plugin({
      state: {
        init() { return 0 },
        apply(tr, value) { return value + 1 }
      }
    })
    
    function getTransactionCount(state) {
      return transactionCounter.getState(state)
    }
  9. Configure groups and marks in a schema

    master

    Schemas can organize nodes into groups (e.g., an "inline" group for both text and inline nodes).

    Marks

    Marks (like bold or links) can be applied to content.

    • Simple Marks: Add a single bit of information (e.g., a <shouting> tag) without attributes.
    • Attribute Marks: Require attributes (e.g., a link's href). The toDOM method must return an array where the second element is an object containing the DOM attributes.

    Mark Inclusion

    By default, marks are inclusive, meaning they apply to content inserted at their end. For marks like links, you should set the inclusive property to false on the MarkSpec to prevent unwanted behavior.

    // PART(starSchema_1)
    // Schema using groups (e.g., 'inline') and specific block types.
    
    // PART(starSchema_2)
    // Schema defining marks with attributes (links) and simple marks (shouting).
  10. Configure Marks for inline content

    master

    Marks add styling (like bold or italic) to inline content. A schema must declare all allowed mark types in its marks property. By default, all marks are allowed on inline nodes, but you can restrict them using the marks property on a node spec.

    • Use "_" as a wildcard to allow all marks.
    • Use "" (empty string) to allow no marks.
    • Use a space-separated string of mark names for specific allowed marks.
    const markSchema = new Schema({
      nodes: {
        doc: {content: "block+"},
        paragraph: {group: "block", content: "text*", marks: "_"},
        heading: {group: "block", content: "text*", marks: ""},
        text: {inline: true}
      },
      marks: {
        strong: {},
        em: {}
      }
    })
  11. Use Slices to represent document ranges

    master

    A Slice represents a range of content between two positions. Unlike a full Node or Fragment, a Slice can have 'open' nodes at its start or end. This happens when a selection starts or ends in the middle of a node (e.g., selecting from the middle of one paragraph to the middle of another).

    An open node in a slice might contain content that violates the schema if treated as a full node, because some required child nodes might exist outside the slice boundaries.

    Key Concepts

    • openStart: The depth of the open node at the start of the slice.
    • openEnd: The depth of the open node at the end of the slice.
    • Node.slice(from, to): Method used to extract a slice from a node or document.
    • Fragment: A slice stores its content as a Fragment.

    Example

    // doc holds two paragraphs, containing text "a" and "b"
    let slice1 = doc.slice(0, 3) // The first paragraph
    console.log(slice1.openStart, slice1.openEnd) // → 0 0
    
    let slice2 = doc.slice(1, 5) // From start of first paragraph
                                 // to end of second
    console.log(slice2.openStart, slice2.openEnd) // → 1 1
    // doc holds two paragraphs, containing text "a" and "b"
    let slice1 = doc.slice(0, 3) // The first paragraph
    console.log(slice1.openStart, slice1.openEnd) // → 0 0
    let slice2 = doc.slice(1, 5) // From start of first paragraph
                                 // to end of second
    console.log(slice2.openStart, slice2.openEnd) // → 1 1
  12. Rebase steps for collaborative editing

    master

    Rebasing is the process of taking a step that was intended for one version of a document and transforming it so it can be applied to a different version (usually a newer one produced by another user).

    To rebase a step, use step.map(mapping). This maps the entire step through a provided mapping. If the step's target content has been deleted or changed in a way that makes the step invalid, step.map() will return null.

    When rebasing chains of steps, the Mapping abstraction handles complex pipelines, including the inverse relations required to navigate through multiple changes and re-insertions.