Skip to content

Instantly share code, notes, and snippets.

@nathanlesage
Last active October 24, 2022 09:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nathanlesage/266f466935308792196d32157f226d39 to your computer and use it in GitHub Desktop.
Save nathanlesage/266f466935308792196d32157f226d39 to your computer and use it in GitHub Desktop.
ContentEditable Demo (CodeMirror 6)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ContentEditable Demo</title>
<script src="index.js"></script>
</head>
<body>
<div id="editor"></div>
</body>
</html>
import { EditorState } from "@codemirror/state"
import { Decoration, DecorationSet, EditorView, ViewPlugin, ViewUpdate, WidgetType } from "@codemirror/view"
import { syntaxTree } from '@codemirror/language'
import { markdown, markdownLanguage } from "@codemirror/lang-markdown"
// Basic Widget class that only creates demo content
class ImageWidget extends WidgetType {
constructor () { super() }
eq (other: ImageWidget) { return false }
toDOM (view: EditorView): HTMLElement {
const wrapper = document.createElement('div')
wrapper.style.display = 'inline-block'
wrapper.innerText = `>>>This is a contenteditable Div<<<`
// The following two lines don't really work. The editor always sets the contenteditable attr to false. One can programmatically overwrite it and edit the div without any problems, however.
// wrapper.contentEditable = 'true'
wrapper.setAttribute('contenteditable', 'true')
const input = document.createElement('input')
input.value = 'An input elem -- edit me!'
wrapper.appendChild(input)
return wrapper
}
ignoreEvent (event: Event): boolean { return true }
}
// Small render function that uses the syntaxTree to find syntax nodes to replace
function renderWidgets (state: EditorState, visibleRanges: readonly {from: number, to: number}[]): DecorationSet {
const widgets: any[] = []
const selections = state.selection.ranges.map(range => [range.from, range.to])
for (const { from, to } of visibleRanges) {
syntaxTree(state).iterate({
from, to,
enter: (node) => {
// Only render widgets with no selection overlaps
const overlaps = selections.filter(([from, to]) => !(to <= node.from || from >= node.to)).length
if (overlaps > 0) { return }
// In this example, we only render images.
if (node.type.name !== 'Image') { return }
const widget = Decoration.replace({ widget: new ImageWidget(), inclusive: false })
widgets.push(widget.range(node.from, node.to))
}
})
}
return Decoration.set(widgets)
}
// This ViewPlugin is basically the decoration example.
const plugin = ViewPlugin.fromClass(class {
decorations: DecorationSet
constructor (view: EditorView) {
this.decorations = renderWidgets(view.state, view.visibleRanges)
}
update (update: ViewUpdate) {
if (update.docChanged || update.viewportChanged || update.selectionSet) {
this.decorations = renderWidgets(update.view.state, update.view.visibleRanges)
}
}
}, { decorations: view => view.decorations }
)
document.addEventListener('DOMContentLoaded', () => {
// Create a basic state that only has the Markdown parser (to retrieve the
// Image syntax nodes) and our demo plugin
const state = EditorState.create({
doc: '# Test document\n\n![Test image](http://www.example.com/image.png)\n\n> A blockquote',
extensions: [
markdown({ base: markdownLanguage }),
plugin
]
})
const view = new EditorView({ state, parent: document.getElementById('editor') ?? undefined })
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment