Skip to content

Instantly share code, notes, and snippets.

@marijnh
Created October 28, 2016 15:15
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save marijnh/e8cfc8b427c97f4a69f324a1bc709819 to your computer and use it in GitHub Desktop.
Save marijnh/e8cfc8b427c97f4a69f324a1bc709819 to your computer and use it in GitHub Desktop.
Demo of a ProseMirror editor that uses a nested code editor
const {EditorState, Selection} = require("prosemirror-state")
const {MenuBarEditorView} = require("prosemirror-menu")
const {DOMParser, DOMSerializer, Schema} = require("prosemirror-model")
const {schema: baseSchema} = require("prosemirror-schema-basic")
const {exampleSetup} = require("prosemirror-example-setup")
const {keymap} = require("prosemirror-keymap")
const CodeMirror = require("codemirror")
require("codemirror/mode/javascript/javascript")
let view, menuView, schema = new Schema({
nodes: baseSchema.nodeSpec.update("code_block", {
group: "block",
attrs: {text: {default: ""},
language: {default: "text/plain"}},
parseDOM: [
{tag: "pre", getAttrs: dom => ({text: dom.textContent, language: dom.getAttribute("data-language") || "text/plain"})},
],
toDOM(node) {
return ["pre", {"data-language": node.attrs.language}, node.attrs.text]
}
}),
marks: baseSchema.markSpec
})
function maybeEscapeCode(sendAction, unit, dir) {
return function(cm) {
let pos = cm.getCursor()
if (cm.somethingSelected() || pos.line != (dir < 0 ? cm.firstLine() : cm.lastLine()) ||
(unit == "char" && pos.ch != (dir < 0 ? 0 : cm.getLine(pos.line).length)))
return CodeMirror.Pass
view.focus()
sendAction((state, pos) => Selection.near(state.doc.resolve(pos + (dir < 0 ? 0 : 1)), dir).action())
}
}
function incrementalNewValue(cm, oldVal, newVal) {
let start = 0, oldEnd = oldVal.length, newEnd = newVal.length
while (start < oldEnd && oldVal.charCodeAt(start) == newVal.charCodeAt(start)) ++start
while (oldEnd > start && newEnd > start &&
oldVal.charCodeAt(oldEnd - 1) == newVal.charCodeAt(newEnd - 1)) { oldEnd--; newEnd-- }
cm.replaceRange(newVal.slice(start, newEnd), cm.posFromIndex(start), cm.posFromIndex(oldEnd), "docUpdate")
}
function codeBlockView(node, sendAction) {
let cm = new CodeMirror(null, {
value: node.attrs.text,
mode: node.attrs.language,
lineNumbers: true,
extraKeys: {
Up: maybeEscapeCode(sendAction, "line", -1),
Left: maybeEscapeCode(sendAction, "char", -1),
Down: maybeEscapeCode(sendAction, "line", 1),
Right: maybeEscapeCode(sendAction, "char", 1)
}
})
cm.getScrollerElement().contentEditable = false
let updating = false, attrs = node.attrs
cm.on("changes", () => {
if (updating) return
attrs = {text: cm.getValue(), language: attrs.language}
sendAction((state, pos) => state.tr.setNodeType(pos, null, attrs).action())
})
setTimeout(() => cm.refresh(), 50)
return {
update(node) {
if (attrs.text != node.attrs.text)
incrementalNewValue(cm, attrs.text, node.attrs.text)
if (attrs.language != node.attrs.language)
cm.setOption("mode", node.attrs.language)
attrs = node.attrs
return true
},
parseRule() {
return {node: "code_block", attrs}
},
dom: cm.getWrapperElement(),
select() {
cm.focus()
return true
}
}
}
menuView = new MenuBarEditorView(document.querySelector("#editor"), {
state: EditorState.create({
doc: DOMParser.fromSchema(schema).parse(document.querySelector("#content")),
plugins: exampleSetup({schema}),
}),
onAction(action) { menuView.updateState(view.state.applyAction(action)) },
handleClickOn(_view, _pos, node) { return node.type.name == "code_block" },
nodeViews: {code_block: codeBlockView}
})
view = window.view = menuView.editor
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment