A WYSIWYG Markdown editor for web pages, as a plain JavaScript component: no framework, no
build step and no dependencies. You edit formatted text; the document stays Markdown. It is
the web counterpart of MaxPad in MarkdownTools: the same layers, the same rules for what each
key does, and a browser version of the VirtualTextView control it is built on.
- Pluggable.
new MarkdownEditor(element, { markdown })from a script, or a<markdown-editor>element in plain HTML. The element is a form field: a form submits its Markdown, like a<textarea>. - WYSIWYG, with the Markdown kept. The syntax is hidden; with the caret in a construct (bold text, a link, a heading's line), its syntax shows, ready to edit, and hides again when the caret leaves. An unedited document round-trips byte for byte, line endings included.
- CommonMark, plus the extensions MaxPad uses: pipe tables, task lists, YAML front
matter,
==underline==,~~strikethrough~~,~subscript~and^superscript^. - Virtualized. Only the paragraphs near the viewport are laid out, so long documents scroll as fast as short ones.
Serve the folder (ES modules are not loaded from file:// pages), then:
<div id="editor" style="height: 480px"></div>
<script type="module">
import { MarkdownEditor } from './src/index.js';
const editor = new MarkdownEditor('#editor', {
markdown: '# Hello\n\nSome **bold** text.',
});
editor.addEventListener('change', () => save(editor.value));
</script>Or, with no script of your own:
<script type="module" src="./src/index.js"></script>
<form>
<markdown-editor name="body" style="height: 320px">
<script type="text/markdown">
## What's new
- **Faster** loading
- [ ] Write the *documentation*
</script>
</markdown-editor>
</form>The editor fills the element it is put in, so give that element a height.
The user manual says everything the editor does, with examples: getting started, writing in it (every key, every kind of Markdown, the raw view), and building it into a page (options, methods, events, the toolbar, themes and stylesheets, the element, recipes).
To run the demos and the tests on Windows, with nothing installed but PowerShell:
powershell -ExecutionPolicy Bypass -File tools/serve.ps1Then open:
- http://localhost:8765/demo/index.html: the editor, with its toolbar and two buttons of the page's own in it: a dark theme and read-only mode.
- http://localhost:8765/demo/element.html:
<markdown-editor>in a form. - http://localhost:8765/demo/tinypad.html: TinyPad, the tutorial's small word processor,
on the bare
VirtualTextView. - http://localhost:8765/test/index.html: the tests (add
?steps=150for a longer random run, or?suites=parser,commandsfor some of them).
new MarkdownEditor(target, options) puts an editor into target (an element, or a selector
of one). The options, all optional:
| Option | Meaning |
|---|---|
markdown |
The document to start with. |
theme |
'light' (the default), 'dark', or a theme object (below). |
toolbar |
true (the default), false, or a list of DEFAULT_TOOLBAR's items and of your own buttons (below). |
readOnly |
The document can be read, selected and copied, not changed. |
revealSyntax |
Show the syntax at the caret (true by default). |
revealUnfocused |
Show it while the editor has no focus too (false by default). |
showFormattingMarks |
Show ¶ at the end of each paragraph and ↵ at a line break. |
imageBase |
The URL relative picture addresses are loaded from. |
openLinks |
Ctrl+click opens a link in a new tab (true by default). |
prompt |
async (question, value) => answer: asks for a link's address. |
rawMode |
Start in the raw Markdown view (below). |
showParagraphNumbers |
Each paragraph's number in a gutter left of the text. |
showParagraphTypes |
Each paragraph's type (Heading 1, Bullet, Code...) in the gutter. |
showOutline |
Collapsible headings: triangles beside them fold what they head (off). |
screenReader |
'auto' (on with a mouse, off on a touch screen), 'on' or 'off': the text at the caret given to a screen reader. |
What a page reads and calls:
| Member | What it is |
|---|---|
value |
The Markdown, in the document's own line endings; set it to load another. |
version |
Counts every change: compare it with the one saved to know if it is dirty. |
selection, select(a, c) |
The selection as offsets in the Markdown. |
undo(), redo() |
Unlimited, of the Markdown; a word typed is one step. |
toggleInline(f) |
'strong', 'em', 'underline', 'strike', 'code', 'sub', 'sup'. |
isInline(f) |
Whether the selection is in that format. |
setBlockType(t) |
'paragraph', 'h1'...'h6', 'quote', 'bullet', 'number', |
'task' or 'code'; again on the same type makes paragraphs again. |
|
blockType |
The type of the paragraph at the caret. |
insertTable() |
Also insertRule(), insertCodeBlock(), insertLink(url), |
insertImage(url, alt). |
|
execute(name) |
Any toolbar command by name ('h2', 'bullet', 'link'...). |
toolbarItems |
The toolbar's items: set it to change which buttons show, and their order. |
refreshToolbar() |
Shows your own buttons' state again (pressed, enabled). |
rawMode, toggleRawMode() |
The raw Markdown view on or off (below). |
showParagraphNumbers |
The gutter's paragraph numbers on or off (the toolbar's # button). |
showParagraphTypes |
The gutter's paragraph types on or off (the toolbar's button beside #). |
showOutline |
Collapsible headings on or off (yours to offer: the default toolbar has no button). |
collapseAll(), expandAll() |
Fold every heading, or open them all. |
paragraphNumber |
The caret's paragraph's number, as the gutter shows it: for a status bar. |
theme, zoom |
Change the look, or the scale, at any time. |
zoomIn(), zoomOut() |
10% larger or smaller (the toolbar's + and -); resetZoom(): 100%. |
readOnly, focus() |
As their names say. |
focusToolbar() |
The focus to the toolbar (Alt+F10), its button last used. |
destroy() |
Takes the editor out of the page. |
Events (addEventListener): change after each change of the Markdown, selectionchange,
modechange (the raw view on or off), toolbarclick (one of your buttons; detail.name),
zoomchange, linkclick (detail.href; call preventDefault() to handle it
yourself), and clipboardunavailable (Shift+Delete or Ctrl+Shift+V could not use the
clipboard: nothing was cut or pasted).
Your own toolbar buttons go in the toolbar list (or toolbarItems) beside the
editor's names, as objects: name, title, icon (an SVG: a whole <svg>, or what goes
in a 24 x 24 one drawn in strokes, as the editor's icons are), or label (text), and
onClick(editor, event); pressed(editor) makes it a switch shown pressed or not, and
enabled(editor) says when it can be clicked. They look like the editor's own buttons, and
the caret stays in the editor.
const lock = {
name: 'readonly', title: 'Read-only',
icon: '<rect x="4" y="11" width="16" height="10" rx="2"/>' +
'<path d="M8 11V7a4 4 0 0 1 8 0v4"/>',
pressed: (editor) => editor.readOnly,
onClick: (editor) => (editor.readOnly = !editor.readOnly),
};
new MarkdownEditor('#editor', {
toolbar: ['undo', 'redo', '|', 'strong', 'em', '|', lock],
});<markdown-editor toolbar="undo redo | strong em"> picks the editor's items by name.
The raw Markdown view (rawMode, the toolbar's last button, Ctrl+Shift+R) shows the
Markdown itself in the editor, as plain text, and lets you edit it there, as MaxPad's View >
Raw Markdown: the caret stays where it was, looking changes nothing, and what you change
there becomes the document when you leave the view, one undo bringing back the document as it
was before. While it is on, value is the text it shows, and undo and redo go an edit at a
time.
A theme object overrides any of LIGHT_THEME's values: fontFamily, fontSize,
codeFontFamily, color, background, headingColor, linkColor, syntaxColor,
codeBackground, quoteBarColor, documentWidth, centered, listIndent and the rest
(see src/markdown/styleProvider.js). Give base: 'dark' to start from the dark theme.
Restyling the content with your own stylesheet. The text is styled by classes of the
editor's default stylesheet, which sits in the CSS cascade layer mde: a rule of your own
stylesheet wins over it whatever its specificity, so you restyle only what you want. Each
paragraph is mde-content-para and its kind's (mde-content-p, mde-content-h1 to -h6,
mde-content-bullet, mde-content-code-block, mde-content-table-cell...); runs are
mde-content-code, mde-content-link, mde-content-strike... The theme's values are custom
properties on .mde-content (--mde-heading-color, --mde-link-color, --mde-font-size...),
and the dark theme is .mde-content.mde-dark. Give sizes in em to have them zoom with the
text (section 10 of documents/implementation.md lists every class).
.mde-content-h1 { font-size: 2.4em; color: #8b0000; }
.mde-content { --mde-link-color: #2e7d32; }
.mde-content-code { border: 1px solid #ccc; }
.mde-content-page { margin: 0; } /* the document at the left */
.mde-content { --mde-document-width: 40em; }The document is centered in the editor and at most 720 px wide (at 100%): the page,
.mde-content-page, has margin: 0 auto and its max-width from --mde-document-width.
| Keys | What they do |
|---|---|
| Enter | A new paragraph; in a list, a new item; on an empty item, out of the list |
| Shift+Enter | A line break inside the paragraph (a backslash hard break) |
| Backspace, Delete | At a block's edge: join, take off a list's marker or a quote level |
| Tab, Shift+Tab | At an item's start: in or out a level; at a heading's: its level |
| Ctrl+B, Ctrl+I, Ctrl+U | Bold, italic, underline |
| Ctrl+`, Ctrl+Shift+X | Inline code, strikethrough |
| Ctrl+K | A link |
| Ctrl+1 to Ctrl+6, Ctrl+0 | Heading 1 to 6, paragraph (Ctrl+Alt+1 to 6 and 0 too) |
| Ctrl+=, Ctrl+- | One heading level deeper (a paragraph becomes Heading 1), shallower |
| Ctrl+Shift+7, 8, 9 | Numbered list, bullet list, task list |
| Ctrl+Alt+Q, C, L | Quote, code block, horizontal line |
| Ctrl+Z, Ctrl+Y | Undo, redo |
| Ctrl+Shift+R | The raw Markdown view on or off |
| Alt+F10 | To the toolbar: Left and Right in it, Escape back |
| Shift+Alt+F1 | Screen reader mode on or off |
| Escape, then Tab | Out of the editor (Tab alone indents) |
| Ctrl and the mouse wheel | Zoom (about 2% a notch) |
The keys are MaxPad's. A browser gives the page Ctrl+1 to Ctrl+6 before it switches tabs with them, and switches none when the page takes them; Ctrl+Alt+1 to 6 are there as well for a browser that keeps them. MaxPad's Ctrl+Shift+T, Q, C, [ and ] are the browser's own, so the lists are on Google Docs' keys and the quote, the code block and the line on Ctrl+Alt.
documents/implementation.md describes how it works, module
by module. In short, the layers are MaxPad's, one module each:
| Here | In MarkdownTools | What it does |
|---|---|---|
markdown/parser.js, inlines.js |
MaxPad.Dom |
Markdown to a tree with source offsets |
markdown/projection.js |
MaxPad.Projection |
The tree as the view's paragraphs |
markdown/commands.js, formatting.js, blocks.js |
MaxPad.Editing |
The keys as edits |
markdown/controller.js, history.js |
MarkdownViewController |
The loop, undo |
markdown/styleProvider.js |
MarkdownStyleProvider |
The default look |
view/VirtualTextView.js |
OptimaxTools.RichText |
The rich text view |
MarkdownEditor.js, element.js |
MaxPad (the window) |
The component |
The source is the authority. The view shows a projection of the Markdown: each paragraph
of the view is the source of its block's lines, with the syntax in it as hidden runs, so a
position in the view is a position in the source plus an offset. Every key the user presses
reaches the controller as an edit command; it works out the new Markdown (each rule is a
pure function of the source and the selection, in commands.js), reads it again, and gives
the view just the paragraphs that changed. The view's own editing never runs for Markdown.
Line endings. A line ending is normalized to the document's own on load: a file whose line endings are a mixture is saved with one kind.
The view is a component of its own, as VirtualTextView is: a list of paragraphs of
runs, styles from a style provider, its own caret, selection, keyboard, mouse, IME and
clipboard handling, and its own editing for a host that wants it (TinyPad). Before it changes
anything it raises a cancelable editcommand event; it tells an editRecorder of each edit
it makes, as a TextEdit that applyEdit replays and inverse() undoes; replaceParagraphs
and restyleParagraph change what it shows without an undo step. It draws text with the
browser's own layout, one element per paragraph near the viewport, and its own caret and
selection over it; a hidden text area takes the keyboard, the input method and the
clipboard.
- find and replace, spelling.
- Code highlighting by language, pasting pictures.
- Table commands beyond typing in cells, Tab and Enter (rows and columns added or deleted, alignment).