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>