How to make your own markdown editor

While developing this blog in Ruby on Rails, I decided to use markdown to style new articles, so blog visitors could have a nice visual expeience (and they could use it in comments, if they wished. I wanted to go lightweight and I wanted it to be easily exensible.

After writing my own javascript markdown toolbar, which I never quite got working with browser events in IE (undo/redo didn’t work in IE), I looked for a better alternative, and came across rangyinputs. Using it’s simple API, and relying on bootstrap for button styles, I was able to complete a very professional looking markdown toolbar in just a few hours, so I thought I’d describe the process here.

toolbar

Step 1: The form fields partial

I’ll need to render the toolbar partial just above any textareas I want use it in.

app/views/articles/_fields.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
  <div class="article_form">
    <%= render 'shared/error_messages', target: @article %>

    <%= f.label :title %>
    <%= f.text_field :title, class: 'form-control' %>

    <%= f.label :body %>
    <%= render 'toolbar' %> <!-- Render toolbar between label and target textarea -->
    <%= f.text_area :body, class: 'form-control' %>

    <%= f.label :tag_list %><br />
    <%= f.text_field :tag_list %>
  </div>
app/views/articles/_toolbar.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<div class="md_editor btn-toolbar" role="toolbar">

  <div class="btn-group" role="group">    
    <button class="btn btn-default" id="add_strong" title="Bold Text">
      <strong>B</strong>
    </button>  
    <button class="btn btn-default" id="add_em" title="Italic Text">
      <em>I</em>
    </button>
  </div>

  <div class="btn-group" role="group">
    <button class="btn btn-default" id="add_h1" title="Heading 1">H1</button>
    <button class="btn btn-default" id="add_h2" title="Heading 2">H2</button>
    <button class="btn btn-default" id="add_h3" title="Heading 2">H3</button>
    <button class="btn btn-default" id="add_h4" title="Heading 2">H4</button>
    <button class="btn btn-default" id="add_h5" title="Heading 2">H5</button>
    <button class="btn btn-default" id="add_h6" title="Heading 2">H6</button>
  </div>

  <div class="btn-group" role="group">
    <button class="btn btn-default" id="add_paragraph" title="New Paragraph">
      <span>P</span>
    </button> 
    <button class="btn btn-default" id="add_blockquote" title="Blockquote">
      <strong>"-"</strong>
    </button>
    <button class="btn btn-default" id="add_unord_list" title="Unordered (bulleted) list">
      <span class="glyphicon glyphicon-list"></span>
    </button>
    <button class="btn btn-default" id="add_ord_list" title="Ordered (numbered) list">
      <span class="glyphicon glyphicon-list-alt"></span>
    </button>
  </div>

  <div class="btn-group" role="group">
    <button class="btn btn-default" id="add_link" title="Text Link">
      <span class="glyphicon glyphicon-link"></span>
    </button>
    <button class="btn btn-default" id="add_url_link" title="Clickable URL Link">&#x3C;url&#x3E;</button>
    <button class="btn btn-default" id="add_img" title="Insert Picture from URL">
      <span class="glyphicon glyphicon-picture"></span>    
    </button>
  </div>
  <div class="btn-group" role="group">
    <button class="btn btn-default" id="add_inline_code" title="Inline Code Snippet">&#x3C;/&#x3E;</button>
    <button class="btn btn-default" id="add_fenced_code" title="Fenced Code Block">&#x3C;/.&#x3E;</button>
  </div>

</div>

note that some of the entities in the code above are used to prevent the characters from being confused with poorly formed HTML tags during rendering on the blog. There are other ways to escape them, and I’m leaving refactoring as an exercise for later.

Step 2: Bootstrap and Jquery

Make sure they are included in: app/assets/javascripts/application.js. You’ll see I included some notes in comments in the application.js file step 4, below.

Step 3: rangyinputs

You can find the project here: https://github.com/timdown/rangyinputs. You’ll need to grab one library file and throw this into your asset pipeline. The rangyinputs-jquery-src.js is a good choice. Well, I like it because it’s the full, formatted source, so I can easily read/edit if needed. Put this in app/assets/javascripts/ or if you prever in vendor/assets/javascripts/. Either of these locations will work as long as the file is included in the asset pipeline.

Note this next section applies only if you use the source/unminified rangyinputs library. If you use the -src.js version of rangyinputs, you may need to do one other thing, currently, assuming you don’t shitcan (technical term) turbolinks. You’ll need to make one small, but very important edit to the rangyinputs library to make it work with turbolinks. Don’t worry, it’s easy. You just need the “$” at the beginning of the first line of the code. So after comments, the file should essentially be like this:

$(function ($) {
   // many lines of nifty code here
}

This is a necessary workaround, probably due to turbolinks. Minor inconvenience.

Step 4: Toolbar Code

This is really the fun part. I’ll include a sample of how I coded my button functions using the libraries API. There is really only function you need, unless you want to get all fancy and stuff. I think you’ll see pretty easily when you read this code how the the button actions are set up. Throw a new button on your toolbar, give it the desired bootstrap button classes, a unique and informative id, and you bind an click action to the button by it’s id. Give it a try. There is little that feels better than the satisfaction of seeing your custom button light up, and insert some unusual characters around some text you’ve selected in a textarea.

app/assets/javascripts/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// FILE: app/assets/javascripts/application.js

// Notice below that jquery, jquery_ujs, and bootstrap are included, as is the whole folder (require_tree).
// That's why dropping the rangyinputs file into the same folder works. :)

//= require jquery
//= require jquery_ujs
//= require bootstrap
//= require turbolinks
//= require_tree .

var $textBox;

// Due to turbolinks, it's necessary to reload on document ready and page load.
// Set 'ready' here, and call it on either document ready or page load (see last two lines of code)
var ready = (function () {
  var theButtons = [
    { id: "#add_strong", before: "**", after: "**"},
    { id: "#add_em", before: "_", after: "_"},
    { id: "#add_h1", before: "\n# ", after: "\n"},
    { id: "#add_h2", before: "\n## ", after: "\n"},
    { id: "#add_h3", before: "\n### ", after: "\n"},
    { id: "#add_h4", before: "\n#### ", after: "\n"},
    { id: "#add_h5", before: "\n##### ", after: "\n"},
    { id: "#add_h6", before: "\n###### ", after: "\n"},
    { id: "#add_paragraph", before: "\n", after: "\n\n"},
    { id: "#add_blockquote", before: "\n> ", after: "\n"},
    { id: "#add_unord_list", before: "\n* ", after: "\n"},
    { id: "#add_ord_list", before: "\n1 ", after: "\n"},
    { id: "#add_link", before: "[", after: "](link_url)"},
    { id: "#add_url_link", before: "<", after: ">"},
    { id: "#add_img", before: "![", after: "](image_url)"},
    { id: "#add_inline_code", before: "```", after: "```"},
    { id: "#add_fenced_code", before: "\n~~~ ruby\n", after: "\n~~~\n"}
  ];

  theButtons.forEach( function (button) {
    $(button.id).on('click', function (e) {
      e.preventDefault();
      insertText(button.before, button.after);
    });
  });

  $textBox = $("#article_body");

  function saveSelection() {
      $textBox.data("lastSelection", $textBox.getSelection());
  }

  $textBox.focusout(saveSelection);

  $textBox.bind("beforedeactivate", function () {
      saveSelection();
      $textBox.unbind("focusout");
  });
});

function insertText(before_text, after_text) {
    $textBox.focus();
    if(typeof $textBox.data('lastSelection') == "undefined") {
      $textBox.data("lastSelection", $textBox.getSelection());
    }
    var selection = $textBox.data("lastSelection");
    console.log(selection);
    $textBox.setSelection(selection.start, selection.end);
    $textBox.surroundSelectedText(before_text, after_text);
}

$(document).ready(ready);
$(document).on('page:load', ready);

I think this button bar is a great starting point. There is some refactoring to do, and probably the insert behavior can be refined in subtle ways, but right now it does well, and as-is it already has some key features:

  • good cross-browser functionality (Chrome, Safari, FF, IE),
  • browser undo/redo works
  • cursor is left in a logical place after insert, which streamlines writing workflow.

Note on Styling: I’ll leave the CSS to you. In the image above, I used Bootstrap 3 toolbar styling, but added a hover style to buttons.

Created 3/14/2015 1:10PM (MDT) | Last Updated 4/24/2015 2:52PM (MDT)

Comments

Log in to add comments.