Markdown Toolbar: Easy, Extensible, Cross-browser, using rangyinputs and Bootstrap for Rails 4.2
/**
* @license Rangy Inputs, a jQuery plug-in for selection and caret manipulation within textareas and text inputs.
*
* https://github.com/timdown/rangyinputs
*
* For range and selection features for contenteditable, see Rangy.
* http://code.google.com/p/rangy/
*
* Depends on jQuery 1.0 or later.
*
* Copyright 2014, Tim Down
* Licensed under the MIT license.
* Version: 1.2.0
* Build date: 30 November 2014
*/
// Add the $ to start the first line of code...
$(function($) {
// Lots of other nifty code between the curly braces you can find here: https://github.com/timdown/rangyinputs
});
// 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: ""},
{ 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);
<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"><url></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"></></button>
<button class="btn btn-default" id="add_fenced_code" title="Fenced Code Block"></.></button>
</div>
</div>
<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>
Warning! This image looks deliciously clickable, but alas... It's just an image. Don't be fooled.
Step 1: The form I use the form helper and render the toolbar in a partial, so I can reuse it in other forms easily. I'll include the form fields partial file in this gist.
Step 2: Bootstrap and Jquery Make sure they are included in app/assets/javascripts/application.js
.
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 not minified, so I can easily read/edited if needed. Anyway, 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, and probably necessary 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. Yes, I could refactor this, possibly switch to haml.
Note on Styling: I'll basically just leave this to you. I like a light gray button with black text that goes dark gray, with white text on hover. I'm pretty sure Bootstrap already throws a nice box-shadow in there on hover also... Of course, if you're not a fan of Bootstrap, you should ignore everything I've mentioned here about bootstrap and just write your own styles.