Friendly Markdown

Imagine you have a site that allows users to enter comments, and you've decided to use Markdown for the comment input. Your target group mostly knows how to use Markdown, and finds it convenient. But you may also have some non-technical users, for whom learning arcane syntactic rules does not come naturally.

Without changing anything in your backend, you can drop in ProseMirror as an alternative input editor. People can even switch between both views as they are editing!

The prosemirror-markdown package defines a ProseMirror schema that can express exactly the things that can be expressed in Markdown. It also comes with a parser and serializer that convert documents in this schema to and from Markdown text.

To abstract the actual editor, we first create a simple interface around a textarea:

class MarkdownView {
  constructor(target, content) {
    this.textarea = target.appendChild(document.createElement("textarea"))
    this.textarea.value = content
  }

  get content() { return this.textarea.value }
  focus() { this.textarea.focus() }
  destroy() { this.textarea.remove() }
}

And then implement the same interface for a Markdown-enabled ProseMirror instance. The in- and output of this interface is still Markdown text, which it internally converts to a ProseMirror document.

import {EditorView} from "prosemirror-view"
import {EditorState} from "prosemirror-state"
import {schema, defaultMarkdownParser,
        defaultMarkdownSerializer} from "prosemirror-markdown"
import {exampleSetup} from "prosemirror-example-setup"

class ProseMirrorView {
  constructor(target, content) {
    this.view = new EditorView(target, {
      state: EditorState.create({
        doc: defaultMarkdownParser.parse(content),
        plugins: exampleSetup({schema})
      })
    })
  }

  get content() {
    return defaultMarkdownSerializer.serialize(this.view.state.doc)
  }
  focus() { this.view.focus() }
  destroy() { this.view.destroy() }
}

Finally, we can wire up some radio buttons to allow users to switch between these two representations.

let place = document.querySelector("#editor")
let view = new MarkdownView(place, document.querySelector("#content").value)

document.querySelectorAll("input[type=radio]").forEach(button => {
  button.addEventListener("change", () => {
    if (!button.checked) return
    let View = button.value == "markdown" ? MarkdownView : ProseMirrorView
    if (view instanceof View) return
    let content = view.content
    view.destroy()
    view = new View(place, content)
    view.focus()
  })
})