Custom markdown editor for string properties in Episerver

This article was migrated from an older iteration of our website, and it could deviate in design and functionality.


Markdown is a light-weight text formatting syntax, suitable for multiple channels and content APIs, not just for web pages. In this post we implement a custom Episerver editor to provide users with a WYSIWYG markdown editor.

Estimated read time : 3 minutes

Jump to

How to use

The editor can be used for any string property, by adding a UIHint attribute:

[UIHint(MarkdownEditorDescriptor.UIHint)]
[Display(Name = "Long description", Description = "Description expressed with markdown")]
public virtual string LongDescription { get; set; }

What it looks like

What it consists of

Our client resources consist of a Dojo widget (Editor.js, Template.html, and Template.css) and an external JavaScript library called SimpleMDE (available on GitHub):

Editor.js

The Editor.js file contains the code for our Dojo widget, i.e. our custom editor:

/* Markdown editor based on SimpleMDE: https://github.com/NextStepWebs/simplemde-markdown-editor
   SimpleMDE is provided under MIT license: https://github.com/NextStepWebs/simplemde-markdown-editor/blob/master/LICENSE */

define([
    "dojo/_base/declare",
    "dojo/_base/config",
    "dojo/_base/lang",
    "dojo/ready",
    "dojo/aspect",
    "dojo/dom-class",
    
    "dijit/_Widget",
    "dijit/_TemplatedMixin",

    "epi/epi",
    "epi/shell/widget/_ValueRequiredMixin",

    "/ClientResources/editors/markdowneditor/simplemde/simplemde.min.js",

    "xstyle/css!./simplemde/simplemde.min.css",
    "xstyle/css!./Template.css"
],

function (
    declare,
    config,
    lang,
    ready,
    aspect,
    domClass,

    _Widget,
    _TemplatedMixin,

    epi,
    _ValueRequiredMixin,

    SimpleMDE
) {
    return declare([_Widget, _TemplatedMixin, _ValueRequiredMixin],
    {
        editor: null, // The SimpleMDE editor object

        templateString: dojo.cache("tedgustaf.editors.markdowneditor", "Template.html"), // Load the widget markup from external template HTML file

        onChange: function (value) {
            /* Summary:
               Notifies Episerver that the property value has changed. */

            this.inherited(arguments);
        },

        constructor: function () {

            /* Summary:
               When the DOM has finished loading, we convert our textarea element to a SimpleMDE editor.
               We also wire up the 'blur' event to ensure editor changes propagate to the widget, i.e. property, value. */

            this.inherited(arguments);

            if (config.isDebug) {
                console.log('Setting up SimpleMDE markdown editor...');
            }

            aspect.after(this, "set", function (name, value) {
                if (name === 'value' && value) {
                    this._refreshEditor();
                }
            }, true);

            ready(lang.hitch(this, function () {

                this.editor = new SimpleMDE({
                    element: document.getElementById("editor-" + this.id),
                    initialValue: this.get('value'),
                    placeholder: this.tooltip,
                    spellChecker: false,
                    status: ["lines", "words" ],
                    toolbar: !this.readOnly ? ["bold", "italic", "heading", "unordered-list", "ordered-list", "link", "preview"] : false
                });

                this.editor.codemirror.on("blur", lang.hitch(this, function () {
                    if (!epi.areEqual(this.get('value'), this.editor.value())) {
                        this.set('value', this.editor.value());
                        this.onChange();
                    }
                }));

                this._refreshEditor();
            }));
        },

        resize: function () {
            /* Summary:
               The resize() function is called when the tab strip containing this widget switches tabs.
               When this happens we need to refresh the editor to ensure it displays property.
               This is a well-known characteristic of CodeMirror, which is part of the SimpleMDE editor. */

            this.inherited(arguments);
            
            this._refreshEditor();
        },

        _refreshEditor: function () {
            /* Summary:
               This function refreshes the editor, and ensures its value matches the current property value.
               It also switches to preview mode, making the editor read-only, if the underlying property
               is in read-only mode. */

            if (!this.editor) {
                return;
            }

            if (typeof this.get('value') !== 'object' && !epi.areEqual(this.editor.value(), this.get('value'))) {
                this.editor.value(this.get('value'));
            }

            if (this.readOnly) {
                var previewElement = this.editor.codemirror.getWrapperElement().lastChild;

                var previewActive = domClass.contains(previewElement, "editor-preview-active");

                if (!previewActive) {
                    this.editor.togglePreview();
                } else {
                    previewElement.innerHTML = this.editor.options.previewRender(this.editor.value(), previewElement);
                }
            }

            this.editor.codemirror.refresh();
        }
    });
});

Template.html

 The widget template is quite trivial. It consists of a textarea element (which the SimpleMDE script will turn into a full-blown markdown editor) and some CSS classes to make the editor appear next to the property name in the "All properties" view in edit mode:

<div class="markdown-editor dijitInline">
    <textarea id="editor-${id}"></textarea>
</div>

Template.css

The stylesheet for our widget template simply sets a suitable width and minimum height for the editor:

.markdown-editor {
    width: 480px;
    font-family: Verdana, Arial;
}

.CodeMirror, .CodeMirror-scroll {
    min-height: 200px; /* To make editor height static (i.e. prevent auto-growth), set 'height' to the desired height */
}

Custom editor descriptor

To enable our custom editor for any string property with our UI hint, we add a class inheriting EditorDescriptor like so:

[EditorDescriptorRegistration(
    TargetType = typeof(string),
    UIHint = UIHint,
    EditorDescriptorBehavior = EditorDescriptorBehavior.PlaceLast)]
public class MarkdownEditorDescriptor : EditorDescriptor
{
    public const string UIHint = "Markdown";

    public override void ModifyMetadata(ExtendedMetadata metadata, IEnumerable<Attribute> attributes)
    {
        base.ModifyMetadata(metadata, attributes);

        metadata.ClientEditingClass = "tedgustaf.editors.markdowneditor.Editor";
    }
}

It's functionality is dead simple: it sets ClientEditingClass, i.e. the Dojo widget type to use for editing the property value, to our custom editor widget.

The  Dojo namespace and type names come from the file and folder structure, assuming a root namespace has been defined in module.config in the site root:

<?xml version="1.0" encoding="utf-8"?>
<module>
  <dojo>
    <paths>
      <add name="tedgustaf" path="" />
    </paths>
  </dojo>
</module>

Demo