Improving the MarkDownDeep jQuery Editor Plugin

I love the MarkdownDeep implementation of Markdown for C# and JavaScript for a few reasons.

  • It is open source. You can customize it if need.
  • They have implementations for both client side editing and server side rendering.
  • Lightweight, jQuery friendly editor. I was having trouble with other markdown editors not playing nice with jQuery. I suspect this was due to timing issues and DOM manipulations. The MarkDownDeep plugin solves this for me.
  • Because of the above, it also plays nice with twitter bootstrap!

So what's not to like? Well as of the time of this post the default CSS styles and DOM structure of the jQuery editor were not ideal. Sure it worked fine, but the toolbar, text area and resizer were all a few pixels different in width because of varying margins/padding with width:100%;. Well I really like percentage widths as I'm getting into responsive web design. So I wanted to fix this.

I first tried just adding my own wrapper divs around the elements, but the jQuery plugin tries to outsmart you and recreates a second toolbar and resizing as siblings to the text area. So the plugin needs some work to support wrapper divs and nice pixel perfect resizing.

Getting Start

Alright the jQuery plugin expects a certain DOM structure. When we go and add wrapper divs, the selector paths for the various editor components will have changed. We need to update the jQuery editor plugin to 1.) Work with the new wrapper divs DOM structure, and 2.) fix the DOM if it doesn't match what was expected.

Wrapping the Text Area

Almost all the selectors in the jQuery plugin were relative to the text area (being the only required initial DOM element). So before we starting working on everything else we need to make sure the text area has a div wrapper.

var editorwrap = $(this).parent(".mdd_editor_wrap");
if (editorwrap.length==0) {
    editorwrap = $(this).wrap('<div class=\"mdd_editor_wrap\" />').parent();
}

From now on, all the jQuery selectors can be relative to the editorwrap element (using children('textarea') when we actually do need the editor itself)

While we're at it let's fix up the editor's CSS styles so that in can truly have 100% width within any given container.

Old Editor CSS Styles:

textarea.mdd_editor
{
    width:100%;
    resize:none;
    margin:0;
}
  1. First off some browser have a default padding on the text area. I don't want to use varying defaults, so lets set an explicit padding:3px; to ensure consistency.
  2. Secondly, now that we have a wrapper div we can account for the borders/padding of the textarea to make 100% sizing work properly. So we add a padding-right: 8px; to the wrapper div so that the textarea's 100% width is actually 8px less than the wrapper div (the border widths plus the paddings).

New Editor CSS STyles:

div.mdd_editor_wrap
{
    padding-right: 8px;
}
textarea.mdd_editor
{
    width:100%;
    resize:none;
    margin:0;padding: 3px;
}

Fixing the Toolbar

Ok so now we want the toolbar to also work nicely with 100% widths. It too will need a wrapper div and a few javascript tweaks. Now because I didn't want to break anyone's code out there that is using the markdowndeep jQuery editor plugin, the fix here is a little more complicated than if we started over from scratch.

Here's how I tweaked the code that checks for and creates the toolbar if necessary:

// Possible cases: 1) wrapper and toolbar exists, 2) only toolbar exists (no wrapper), 3) nothing exists
var toolbarwrap=editorwrap.prev(".mdd_toolbar_wrap"),
    toolbar = editorwrap.prev(".mdd_toolbar");
if (toolbarwrap.length==0) {
    // Does the toolbar exist?
    if (toolbar.length==0)
    {
        toolbar=$("<div class=\"mdd_toolbar\" />");
        toolbar.insertBefore(editorwrap);
    }
    // Add our wrapper div (whether or not we created the toolbar or found it)
    toolbarwrap = toolbar.wrap('<div class=\"mdd_toolbar_wrap\" />').parent();
} else {
    // wrapper was there, how about the toolbar?
    if (toolbar.length==0) {
        // No toolbar div
        toolbar=$("<div class=\"mdd_toolbar\" />");
        // Put the toolbar inside the provided wrapper div
        toolbarwrap.html(toolbar);
    }
}
// Stuff the toolbar with buttons!
toolbar.append($(MarkdownDeepEditorUI.ToolbarHtml()));

This code will accept any variation of conditions of DOM elements pre-existing in the markup and will come out the other end with exactly the structure we want with a wrapper div around the toolbar div.

Once we have a nice wrapper div in place, we can go ahead and fix the sizing again so the toolbar will work properly with 100% width sizing.

Original Styles:

div.mdd_toolbar
{
    float:left;
    padding:5px;
    width:100%;
    height:20px;
}
  1. Remove width:100%; and float:left;. The toolbar div will now behave like a normal div and expand to fill the space provided (less its margins and padding).
  2. Add div.mdd_toolbar_wrap style block and add the width: 100%; here. The wrapper div sets the actual total width, the toolbar div fills that up while respecting the padding/marign/height of the toolbar div.

New toolbar styles:

div.mdd_toolbar_wrap 
{
    width:100%;
}
div.mdd_toolbar
{
    padding:5px;
    height:20px;
}

The toolbar looks great! Now the only problem is the buttons don't work anymore! That's because we broke some jQuery selector paths by changing the DOM around. So lets go and tweak the core MarkdownDeepEditorUI toolbar button handler to use the new DOM structure.

In the onToolbarButton function we just need to change the selector to

var editor = $(e.target).closest("div.mdd_toolbar_wrap").next('.mdd_editor_wrap').children("textarea").data("mdd");

Now the buttons will properly go up to the toolbar wrap, next to the editor wrapper, and down the editor itself and invoke the appropriate command.

Fixing the Resizer

Alright this is getting a little repetitive, but yes we need a wrapper div on the resizer to support our pixel perfect widths even with percentage based layouts. The javascript tweaks are going to look very similar to what we did to the textarea.

resizerwrap=editorwrap.next(".mdd_resizer_wrap"),
resizer=(resizerwrap.length==0)?editorwrap.next(".mdd_resizer"):resizerwrap.children('.mdd_resizer');
if (resizerwrap.length==0) {
    if (resizer.length==0)
    {
        resizer=$("<div class=\"mdd_resizer\" />");
        resizer.insertAfter(editorwrap);
    }
    // Add our wrapper div (whether or not we created the toolbar or found it)
    resizerwrap = resizer.wrap('<div class=\"mdd_resizer_wrap\" />').parent();
} else {
    if (resizer.length==0) {
        resizer=$("<div class=\"mdd_resizer\" />");
        resizerwrap.html(resizer);
    }
}
resizerwrap.bind("mousedown", MarkdownDeepEditorUI.onResizerMouseDown);

Now that we have a nice wrapper div, lets fix the CSS again to support pixel perfect widths at 100%.

Original resizer CSS Styles:

div.mdd_resizer
{
    background:#f8f8f8; 
    background-image:url("mdd_gripper.png");
    background-position:center center;
    background-repeat:no-repeat;
    padding-left:2px;
    padding-right:2px;
    width:100%;
    height:9px;
    border:solid 1px #d0d0d0;
    margin-top:-1px;
    cursor:n-resize;
}

New Resizer CSS Styles:

div.mdd_resizer_wrap
{
    width:100%;
}
div.mdd_resizer
{
    background:#f8f8f8; 
    background-image:url("mdd_gripper.png");
    background-position:center center;
    background-repeat:no-repeat;
    padding-left:2px;
    padding-right:2px;
    height:9px;
    border:solid 1px #d0d0d0;
    margin-top:-1px;
    cursor:n-resize;
}

Now same as before, we will have broken a few jQuery selector paths. Back up in our core MarkdownDeepEditorUI functions we need to modify the onResizerMouseDown to search for the closest .mddresizerwrap first. That way regardless of whether your browser fires this event on the inner or outer resizer div jQuery.closest will find the outer mddresizerwrap element. Then we can find the editor wrap and then finally the textarea.

textarea = $(srcElement).closest('.mdd_resizer_wrap').prev('.mdd_editor_wrap').children("textarea")[0],

Why not put a wrapper div around .mdd_preview?

I didn't put a wrapper div around .mdd_preview as the plugin already supports the option to provide your own explicit selector. If you want to get fancy and the next the preview in another div with borders or scroll bars or whatever, you can just set a customer selector as per the docs.

Testing It Out

Alright with our tweaks in place, everything should look just perfect regardless of what the MarkDownDeep jQuery editor is contained in. As a simple test, I'm going to open up the included sample project and wrap the editor in a div like this <div style="width:800px;margin: 0 auto;">.

The toolbar, text area and resizer should all be exactly 800px in width including any borders/padding/margins that the inner components have defined. Looks nice doesn't it?

I have submitted a pull request to the MarkdownDeep github project, so hopefully you will see these changes incorporated into the main build soon!