For two different projects I found myself needing a nice full screen Markdown editor that works well in browsers. After not really finding a finished solution that I liked I ended up rolling my own with a combination of a few different systems.

I had a few requirements since I was also going to be a user of the editor, so here are the main features.

Main Features

  • Automatic/live preview
  • Ctrl + S to save
  • Code support via highlight.js
  • Unsaved changes notification.
  • Markdown help popup

markdown editor window preview

Read on to make your own!

The Page

It's pretty basic. It relies on bootstrap for some simple styling, the rest is done inline.

You'll notice it uses the ASP.NET Ajax form method. It would be simple enough to change this to do the POST via jQuery and insert the response, but this wraps it up nicely and has clean fall back for users with JavaScript disabled.

For JavaScript it uses the pagedown Markdown JavaScript which can be downloaded from that link. I use it in conjunction with the MarkdownDeep NuGet package which includes a .NET markdown parser to prevent forcing the browser to parse and render markdown which on some platforms can be a little slow and awkward. It also requires jQuery and the jquery.unobtrusive-ajax.js files to be loaded. To enable highlight.js support you'll need to also include the JavaScript and CSS for highlight.js which can be downloaded from its page.

<html>
<head>
    <title>Markdown Editor</title>
    <link rel="stylesheet" type="text/css" href="//maxcdn.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" />
</head> 
<body>
    <div style="background: whitesmoke; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden;">
        <!-- Set to your action and controller names -->
        @using (Ajax.BeginForm("MyPostAction", "Controller", null, new AjaxOptions { HttpMethod = "POST", InsertionMode = InsertionMode.Replace, UpdateTargetId = "status-message" }))
        {
            if (ViewBag.HasErrors ?? false)
            {
                <div style="position:absolute;z-index: 100;width:400px;top: 100px;left: 80px;" class="alert alert-danger" id="errors">
                    <span class="btn" onclick="$('#errors').hide()" style="float:right">Close</span>
                    @Html.ValidationSummary("Was unable to save the post for the following reasons:")
                </div>
            }

            <div style="width:100%;background: #333;padding: 6px;color: #fff;">
                <span class="btn btn-default" onclick="$('#markdownhelp').show()" style="margin-right: 10px; float: right; margin-top: 5px;">Markdown Help</span>
                <!-- change to match where you want cancel to go to -->
                <a href="@Url.Action("Edit", new { page = Model.PageUrl })" class="btn btn-info" style="margin-right: 10px; float: right; margin-top: 5px;">Close</a>
                <input type="submit" class="btn btn-success" style="margin-right: 10px; float: right; margin-top: 5px;" value="Save" />
                <h3 style="margin-left: 16px;margin-top: 10px;">
                    Markdown Editor
                    <!-- set ViewBag.Saved in the POST controller method to show the success message on post back when not using ajax -->
                    @if (ViewBag.Saved ?? false)
                    {
                        <span class="alert alert-success" style="display: inline-block; margin: 0; padding: 0;">Saved!</span>
                    }
                </h3>
                <span class="alert alert-success" style="margin: 0 auto; position: absolute; padding: 10px; top: 8px; left: 32%;" id="status-message">Press Ctrl + S To Save</span>
            </div>
            <div style="width: 48%; height: 92%; display: inline-block; ">
                <h4 style="margin-left: 16px;">Markdown</h4>
                <!-- change name to match name for your form element. also change model field to match your model field -->
                <textarea name="PostMarkdown" id="wmd-input" style="margin-left: 16px; height: 95%; width: 100%; padding: 6px; border: none; font-family: monospace">@Model.PostMarkdown</textarea>
            </div>
            <div style="width: 48%; height: 93%;  display: inline-block; float: right; padding: 8px; ">
                <h4 style="margin-top: 3px;">Preview <span id="wordcount"></span></h4>
                <div style="background: #fff; overflow: scroll; height: 95%;padding:6px" id="wmd-preview"></div>
            </div>
            <div id="markdownhelp" style="position: absolute;top: 65px;right: 0px;padding: 20px;border: 1px solid rgb(51, 51, 51);border-radius: 6px;background-color: rgb(255, 255, 255);display: none;">
                <span class="btn btn-default" onclick="$('#markdownhelp').hide()" style="float: right; margin-bottom: 14px;">Close</span>
                <section class="markdown-help-container">
                    <table class="table table-bordered table-condensed">
                        <thead>
                            <tr>
                                <th>Result</th>
         <th>Markdown</th>
                            </tr>
                        </thead>
                        <tbody>
                            <tr>
                                <td><strong>Bold</strong></td>
                                <td>**text**</td>
                            </tr>
                            <tr>
                                <td><em>Emphasize</em></td>
                                <td>*text*</td>
                            </tr>
                            <tr>
                                <td><code>Inline Code</code></td>
                                <td>`code`</td>
                            </tr>
                            <tr>
                                <td>Strike-through</td>
                                <td>~~text~~</td>
                            </tr>
                            <tr>
                                <td><a href="#">Link</a></td>
                                <td>[title](http://)</td>
                            </tr>
                            <tr>
                                <td>Image</td>
                                <td>![alt](http://)</td>
                            </tr>
                            <tr>
                                <td>List</td>
                                <td>* item</td>
                            </tr>
                            <tr>
                                <td>Blockquote</td>
                                <td>&gt; quote</td>
                            </tr>
                            <tr>
                                <td>H1</td>
                                <td># Heading</td>
                            </tr>
                            <tr>
                                <td>H2</td>
                                <td>## Heading</td>
                            </tr>
                            <tr>
                                <td>H3</td>
                                <td>### Heading</td>
                            </tr>
                            <tr>
              <td>H4</td>
                                <td>#### Heading</td>
                            </tr>
                            <tr>
                                <td>H5</td>
                                <td>##### Heading</td>
                            </tr>
                            <tr>
                                <td>H6</td>
                                <td>###### Heading</td>
                            </tr>
                            <tr>
                                <td>Table</td>
                                <td>
                                    | Header 1 | Header 2 |<br />
                                    | ------------- |:-------------|<br />
                                    | Data 1 | Data 2 |
                                </td>
                            </tr>
                        </tbody>
                    </table>
                    For further Markdown syntax reference: <a href="http://daringfireball.net/projects/markdown/syntax" target="_blank">Markdown Documentation</a>
                </section>
            </div>

        }
    </div>
    <!-- Add includes for jQuery, highlight.js, and the pagedown markdown JavaScript here -->

    <script>

        var loaded = false;

        $(document).ready(function () {
            var converter = Markdown.getSanitizingConverter();
            Markdown.Extra.init(converter);
            var editor = new Markdown.Editor(converter);

            // bind the on the refresh hook to find unsaved changes.
            editor.hooks.chain("onPreviewRefresh", function (text) {
                if (loaded) {
                    $("#status-message").html("<span style='color:red'>Unsaved Changes Detected!</span>");
                }

                return text;
            });

            editor.run();
            // forces preview window to render
            editor.refreshPreview();

            // binds the code view's scroll to the preview scroll
            // but not vice versa. this could be way better.
            $("#markdown-input").scroll(function () {
                $("#markdown-preview").scrollTop($("#markdown-input").scrollTop());
            });

            // on ctrl + s perform form submit
            $(window).keydown(function (event) {
                loaded = true;
                if (event.ctrlKey && event.keyCode == 83) {
                    $("#status-message").html("<span style='color:#f60'>Saving...</span>");
                    $("form").submit();
                    event.preventDefault();
                    return false;
                }
            });
        });
    </script>
</body>
</html>

And that's it! Have any issues or questions feel free to post in the comments below. I made this is a couple of hours, the major failures or it are the scrolling. Scrolling in the preview window doesn't move the markdown window, and I've noticed with images the views can get way off. It should be percentage based, I think that would be a lot more accurate.

About Author

Siva Katir

Siva Katir

Senior Software Engineer working at PlayFab in Seattle Washington.