A Markdown editor inputfield for ProcessWire, built on EasyMDE.
About the name
The module is called InputfieldSimpleMDE for historical reasons — it originally
shipped SimpleMDE, which
has been unmaintained since 2017. As of version 2.0.0 it ships EasyMDE, the
maintained fork, which brings a current CodeMirror and Marked with it.
The class name stays the same on purpose: renaming it would orphan every field already using this inputfield, on every site. Upgrading is a drop-in — see Upgrading.
Requirements
ProcessWire 3.0 or newer, PHP 5.4 or newer.
Installation
In the admin, Modules → Add New, and install by class name
InputfieldSimpleMDE.
Or install manually: copy the module folder to
/site/modules/InputfieldSimpleMDE/, then Modules → Refresh → Install.
Usage
Create a Textarea field and set Inputfield Type to Simple Markdown Editor on the field's Details tab.
Markdown is stored as-is; nothing is converted on save. For output, apply a
textformatter to the field — the core TextformatterMarkdownExtra module, or
Parsedown, both work.
Alternatively, format it at output time:
echo $sanitizer->entitiesMarkdown($page->your_field, ['fullMarkdown' => true]);For images, Image Tags or Hanna Code work well alongside it.
Editor height
The field's Rows setting, on the Input tab, sets how tall the editor starts. It behaves like a textarea's rows: the editor opens at that height and grows with its content, up to a cap of 300px.
Before version 2.0.0 the Rows setting did nothing — the editor was frozen at 300px whatever the field asked for, which is why short fields such as a field description always looked oversized.
To change the cap, or to set an explicit height and ignore Rows entirely, use the options below:
"minHeight": "120px"
"maxHeight": "200px"maxHeight fixes the height and makes the editor scroll rather than grow.
Configuration
By default the field needs no configuration and none is required to keep working as it always has.
Optionally, on the field's Input tab, EasyMDE options takes a JSON fragment that is merged over the defaults — so you only supply the keys you want to change and the rest of the stock configuration stays put:
"toolbar": ["bold", "italic", "heading", "|", "preview"],
"sideBySideFullscreen": falseKeys must be in double quotes. The surrounding braces are optional. Invalid JSON is logged to the browser console and ignored, so a typo costs you the option, not the editor. The full list of options is in the EasyMDE documentation.
The option is context-aware: it can be set per template under Setup → Templates → (template) → (field) → Overrides.
The defaults are:
| Option | Default |
|---|---|
toolbar | bold, italic, heading, quote, lists, link, image, preview, side-by-side, fullscreen, table, horizontal rule, code, guide |
spellChecker | false (the browser's native spellcheck still applies) |
promptURLs | true |
autoDownloadFontAwesome | false — see below |
FontAwesome
The toolbar icons are FontAwesome classes, so they render in whatever FontAwesome your admin already loads. Install FontAwesome Pro and the toolbar picks up its styling automatically, with nothing to configure.
The module sets autoDownloadFontAwesome: false deliberately. Left unset, the
library looks for a stylesheet whose href contains
//maxcdn.bootstrapcdn.com/font-awesome/ and, not finding one, appends a
<link> to that CDN. It only recognises FontAwesome served from that single
host, so a locally hosted copy — which is what every ProcessWire admin theme
uses — never satisfies the check, and it fires on every page. It also runs per
editor, so a page with ten Markdown fields appended ten of them.
That means an outbound request from your admin to a third-party host on every
page load. SimpleMDE behaved identically, so any site that ran an earlier
version of this module was doing it too; it had simply never been switched off.
If you have a Content Security Policy or a privacy audit that flagged
maxcdn.bootstrapcdn.com, this is where it came from.
Note that a module constructing the editor itself — via the window.SimpleMDE
alias — bypasses these defaults and needs to pass autoDownloadFontAwesome: false in its own options. Field Descriptions Extended does construct its own,
and passes it.
The spell checker
spellChecker is false by default, and that is the only reason the editor
makes no other outbound request. Enabled, the bundled spell checker fetches its
dictionaries from cdn.jsdelivr.net at runtime — two files per editor, from a
third party, in your admin.
The browser's own spell checking is on regardless (nativeSpellcheck), works
offline, and already knows the editor's language, so there is rarely a reason to
turn the bundled one on. If you do, do it knowingly:
"spellChecker": trueHow the editor gets initialised
Worth knowing if you are debugging, because it does not work the way most ProcessWire inputfields do.
The module is autoload => 'template=admin' and loads its assets from init()
rather than renderReady(). ProcessWire only calls renderReady() when a field
actually renders, so loading here as well puts the init script on every admin
page whether or not anything rendered, and a textarea arriving later by AJAX
always has something waiting for it.
This is belt-and-braces rather than the fix for the collapsed-repeater bug,
despite how it looks. Measurement says a repeater renders a hidden prototype
item and even an AJAX-collapsed one still takes its inner fields through
renderReady(), so on any page holding a Markdown field the assets arrive that
way regardless. The repeater bug was actually fixed by the rewrite of
InputfieldSimpleMDE.js: the old double-init guard compared jQuery .data()
against the string 'true' after .data() had already coerced it to a boolean,
so it never held, and initialisation depended on guessing which ProcessWire
event would fire and when.
The trade-off is that EasyMDE loads on every admin request, not only on pages holding a Markdown field. That is deliberate: gating it on the current Process would drop the library in exactly the cases that are hard to predict — custom Process modules, and third parties that ask for the library themselves.
InputfieldSimpleMDE.js then uses a MutationObserver rather than binding to
ProcessWire events, so an AJAX-loaded repeater item, a cloned item, a re-sorted
one, a language tab and a plain page load are all the same case. An
IntersectionObserver re-measures each editor when it first becomes visible,
which is what a CodeMirror instance built inside a hidden container needs.
For diagnostics, in the browser console:
InputfieldSimpleMDE.report() // library loaded? textareas found? editors built?
InputfieldSimpleMDE.scan() // force a pass over the document
InputfieldSimpleMDE.instance('Inputfield_body') // the EasyMDE object for a fieldThe EasyMDE instance is also on the textarea element itself, as
element.simplemdeInstance.
Tests
tests/run.sh runs a browser suite and a PHP suite. Neither needs anything
installed beyond Chrome, Python 3 and PHP.
cd tests
./run.sh # both suites
./run.sh --open # open the browser suite in your own browser
./run.sh --mutate # check the suites can actually failtests/MANUAL.md lists the handful of checks that need a real admin — the Field
Descriptions Extended integration, a save round-trip, real repeater AJAX,
language tabs, fullscreen in other admin themes, and the upgrade path.
See tests/README.md for how it works.
Interoperability
Other modules calling new SimpleMDE(...). The library this module loads is
registered as window.SimpleMDE as well as window.EasyMDE, so code written
against the old global keeps working. Field Descriptions
Extended relies on
this: its Enable SimpleMDE option asks this module for the library and then
constructs an editor on the field description textarea itself.
Multi-language fields. Each language renders its own textarea and gets its own editor, with the field's configured options.
Adding a toolbar button from another module. Each editor fires a
simplemde:built event on its own textarea as soon as it is constructed. The
event bubbles, so one delegated listener covers every field on the page:
function enhance(mde, el) {
// mde = the EasyMDE object, el = the textarea it was built on
var button = document.createElement('button');
button.className = 'my-module-button';
button.title = 'Insert a token';
button.textContent = '\u2726';
button.onclick = function() { mde.codemirror.replaceSelection('[[token]]'); };
mde.gui.toolbar.appendChild(button);
}
// Editors built from now on.
document.addEventListener('simplemde:built', function(e) {
enhance(e.detail.instance, e.detail.element);
});
// Editors that already existed when this script ran.
InputfieldSimpleMDE.editors().forEach(function(ed) {
enhance(ed.instance, ed.element);
});Both lines are needed, and the second is the one people miss. An event only
reaches listeners that were already registered, and the editors on a normal page
are built during this module's own start-up. A module that registers inside
$(document).ready() — the usual ProcessWire pattern — is too late for those,
and hears only about editors created afterwards. The result is a feature that
appears inside AJAX-loaded repeater items and silently never on ordinary fields,
which is a miserable thing to debug.
InputfieldSimpleMDE.editors() returns {instance, element} for every editor
built so far — the same pair the event carries in its detail, which is why both
paths above can hand the same two arguments to one function.
Register the listener before calling editors(), as above, so an editor built
between the two lines is caught rather than missed. It may then be enhanced
twice, so guard with a class or flag if your code is not idempotent.
Do not scan the DOM on a timer instead. Editors are built from a
MutationObserver, so an AJAX-loaded repeater item, a cloned item or an unopened
language tab each produce another one at a moment nothing else can predict.
A few details worth knowing:
- jQuery works too. ProcessWire bundles jQuery 1.12, which copies
detailonto its event object, so$(document).on('simplemde:built', fn)gives youe.detail.instancedirectly with nooriginalEventunwrapping. - A listener that throws cannot break anything.
dispatchEventdoes not propagate a listener's exception to the dispatcher, so the error surfaces in the console and both that editor and the rest of the page's editors are built as normal. That is the browser's guarantee rather than this module's, and the test suite checks it holds. - The event fires after the editor is fully set up, including its height, so measurements taken in a listener are the final ones.
- A button appended this way inherits EasyMDE's toolbar behaviour, including being disabled while the preview is open.
Upgrading from 1.x
Nothing to do beyond replacing the module folder and running Modules → Refresh. Field settings, stored content and the default appearance are unchanged; existing fields keep working without being touched.
Two things to be aware of if you have customised it:
- Custom CSS. EasyMDE wraps the editor in
.EasyMDEContainerand scopes its own rules to it, so a bare.CodeMirror { ... }override of yours now loses on specificity. Prefix such rules with.EasyMDEContainer. - Heading sizes. Headings are now visibly larger in the editor, which they
never were under SimpleMDE. If a line of yours suddenly looks like a heading,
check for a line of dashes directly beneath it — that is a setext heading by
the CommonMark spec, and always was. A blank line before the dashes makes it a
horizontal rule instead. Override
.cm-s-easymde .cm-header-Nin your admin CSS to change the scale. - The vendor files moved.
simplemde.min.js/simplemde.min.cssat the module root are gone, replaced byeasymde/easymde.min.jsandeasymde/easymde.min.css. Anything hard-coding those paths needs updating.
Changelog
2.1.0
- Added a
simplemde:builtevent so another module can extend each editor as it is created — adding a toolbar button, for example. Editors are built from aMutationObserver, at moments nothing outside this module can predict, so an event is the only thing that catches all of them. - Added
InputfieldSimpleMDE.editors(), which reports the editors already built. A listener registered inside$(document).ready()is too late for the fields on the page, so without this the event would reach AJAX-loaded repeater items and silently nothing else. See Interoperability.
2.0.0
- Replaced SimpleMDE 1.11.2 (unmaintained since 2017, bundling CodeMirror 5.15.2) with EasyMDE 2.21.0 (CodeMirror 5.65, Marked 4).
window.SimpleMDEis aliased to EasyMDE so dependent modules keep working.- Editing now marks the field as changed, so ProcessWire's "unsaved changes" confirmation covers it. Previously you could edit a Markdown field, navigate away and lose the work with no warning.
- Optional per-field EasyMDE options, merged over the defaults, settable per template context.
- Fullscreen and side-by-side now escape repeater and image-edit stacking contexts.
- The marker class is applied unconditionally rather than only when the
sizeattribute is empty — a leftover fromInputfieldTextthat could silently disable the editor. - Reduced the MutationObserver's work: mutations CodeMirror makes to its own DOM are ignored and scans are coalesced per animation frame, so typing no longer triggers a document scan per keystroke.
- Editors are released when their field genuinely leaves the document.
- Replaced EasyMDE's heading sizes with a fixed scale. EasyMDE sizes headings
with
calc(1.375rem + 1.5vw)— about 2.5x body text, and it rescales with the browser window, which no other part of a fixed-width admin field does. SimpleMDE never styled heading sizes at all. Headings are now 1.6em down to 1em, proportional to the field's own text and stable at any window size. - The field's Rows setting now sets the editor's starting height. It was
rendered on the textarea and offered on every config screen, but the editor
ignored it: the library set
min-height: 300pxinline and the module's CSS capped the same element at 300px, freezing every editor at exactly 300px. A three-row field description got the same box as a body field. - Stopped fetching FontAwesome from a third-party CDN. The library only ever
recognised FontAwesome loaded from
maxcdn.bootstrapcdn.com, so a locally hosted copy never counted and it appended a<link>to that CDN on every page, once per editor. SimpleMDE did the same, so this predates the fork. - Added a
LICENSEfor the module itself (MIT).
1.1.0
- Fixed initialisation inside repeaters.
License
The module is MIT licensed — see LICENSE.
The bundled EasyMDE library is MIT licensed by its own authors — see easymde/LICENSE.
More modules by Macrura
- PrevNextTabs by Macrura
- InputfieldAceExtended by Macrura, Horst Nogajski
- WireMailMailgun by Macrura
- SettingsFactory by Macrura
- AdminHelp by Macrura
Soundmanager2 Audio for Proceswire
Use file field to add audio files, use shortcode to insert into RTE.6TextformatterSoundmanager by MacruraImport External Images
For content pasted in textarea fields, this will import external images to the images field.6ImportExternalImages by Macrura- TextInputAwesomplete by Macrura
- InputfieldSelectize by Macrura
Install and use modules at your own risk. Always have a site and database backup before installing new modules.