Notes on ProseMirror

🌿 Growing Note ProseMirror

Make editor read-only / Prevent edit

Use InputRule to implement Markdown shortcuts

A helper function that creates an InputRule to add marks.
import { Mark, MarkType, Schema } from 'prosemirror-model'
import { EditorState, Transaction } from 'prosemirror-state'
import { InputRule } from 'prosemirror-inputrules'

/**
 * Build an input rule for automatically marking a string when a given
 * pattern is typed.
 *
 * References:
 * https://github.com/benrbray/prosemirror-math/blob/master/src/plugins/math-inputrules.ts
 * https://github.com/ProseMirror/prosemirror-inputrules/blob/master/src/rulebuilders.js
 */
export function markingInputRule(
  pattern: RegExp,
  markTypeOrMarkTypes: MarkType | MarkType[]
): InputRule {
  return new InputRule(
    pattern,
    (state: EditorState<Schema>, match, start, end) => {
      let marks: Mark[] = []
      if (Array.isArray(markTypeOrMarkTypes)) {
        const markTypes = markTypeOrMarkTypes
        marks = markTypes.map(mt => mt.create())
      } else {
        const markType = markTypeOrMarkTypes
        marks = [markType.create()]
      }

      const textNode = state.schema.text(match[1], marks)
      let tr = state.tr.replaceRangeWith(start, end, textNode)
      marks.forEach(m => {
        tr = tr.removeStoredMark(m) as Transaction<Schema>
      })
      return tr
    }
  )
}
Create an InputRule that add nodes.
import { Slice, Fragment } from 'prosemirror-model'
import { InputRule } from 'prosemirror-inputrules'

/**
 * Some setup may convert "--" to "—" (em dash) first,
 * so we match two cases.
 */
new InputRule(/^(---|—-)$/, (state, _match, start, end) => {
  const schema = state.schema
  const horizontalRuleNode = schema.node(
    schema.nodes.horizontal_rule
  )
  const paragraphNode = schema.node(schema.nodes.paragraph)
  const fragment = Fragment.from([
    horizontalRuleNode,
    paragraphNode,
  ])
  const slice = new Slice(fragment, 0, 0)
  /**
   * Also add a paragraph node below, so that ProseMirror
   * can move caret there.
   */
  return state.tr.replaceRange(start, end, slice)
})

Indent / Un-indent list items

Find parent nodes

Disable browser's "Tab" behavior

Code editors and many modern rich text editors map Tab key to indentation and Shift + Tab key combination to un-indentation. However, in the browser, Tab key is also used to navigate through focusable elements like links, inputs, and buttons. If we don't override it, whenever we hit Tab the editor loses focus.

This is a ProseMirror Plugin that achieve our goal. Note that Tab key is universal, so we need to listen to window.
import { Plugin } from 'prosemirror-state'

export function disableDefaultTabBehavior(): Plugin {
  function preventTab(e: KeyboardEvent) {
    if (e.key === 'Tab') {
      e.preventDefault()
      e.stopPropagation()
    }
  }

  let setupComplete = false

  return new Plugin({
    view: () => ({
      update: () => {
        if (setupComplete) return

        window.addEventListener('keydown', preventTab)
        setupComplete = true
      },
      destroy: () => {
        window.removeEventListener('keydown', preventTab)
      },
    }),
  })
}

An EditorView blurs when a nested EditorView gets focus

An issue I encounter when integrating benrbray/prosemirror-math with Jade.

Can we really not have inputs or editable content inside the editor?
Browsers have a single focused element. There’s no concept of nested focus or something like that. Even if you move your focusable field outside of the editor, focusing it will blur the editor. ~ by marijn

So I need a way to unify the focus state information (treat all nested EditorView as one element), i.e. to know if the focused element is a descendant of the top-level EditorView.

Custom rendering and behavior logic for nodes and marks

Write a Plugin (actually, write a PluginSpec) that provides NodeViews.

About marks

Using Markdown as the persistence format

Do not call coordsAtPos() with a position that was get some time before.

Misc