ABYX

Build a MarkDown editor with JavaScript and HTML

Recently, I've been building a MarkDown editor in HTML and JavaScript and I'm going to describe the process of building this here. It's not that hard to build a text editor with GUI in HTML. The final result looks like this:

MarkDown editor

The HTML and CSS

First, we're going to create the HTML-code necessary for this editor to work. We need a textarea were we can put our message and lay-out in.

Note that the form-element that's wrapped around the textarea has no extra attributes to delegate the post-requests because we'll manage these by JavaScript later (JavaScript gives us some extra control over the submit-process and enables us to check for faulty input). The textarea has a height of 8 rows, but this can be adjusted to suit your needs. The label makes it a bit clearer for the user what should be typed into this textarea.

Now we're going to insert some buttons. I will be adding a bold, italic and header-button, you can add more buttons later using the same method. We're using the Bootstrap framework because it contains some very convenient symbols, namely the Glyphicons that can be used to create clear buttons. The Bootstrap grid is also very handy, but that's something that we don't need here. The HTML looks like this after adding the buttons.

How do I use MarkDown to style text?

Bootstrap glyphicons have to be inserted using the span-tag. Every button has the btn-class which is defined by the Bootstrap framework and is responsible for the styling of this button. Without it, it would look completely gray. Other glyphicons can be found here. There's no suitable glyphicon for the header-button, but we can make our own by using a nice font and the "H1" text. The CSS that takes care of the lay-out looks like this:

.button-text {
    display: inline-block;
    font-weight: 700;
    font-family: "Palatino Linotype", "Book Antiqua", Palatino, serif;
}

.bottom-padded {
    padding-bottom: 5px;
}

The button-text class takes care of the styling for our selfmade H1-button. The bottom-padded class creates a little spacing between the row of buttons and the textarea to make it look a little better. The Palatino font is a web-safe font that gives us the classic serif look for our H1-button. Up until now, we've described the whole styling-process.

The JavaScript

Now it's time to put some logic behind these buttons by using JavaScript. We will be using plain JavaScript, so you don't need some sort of framework such as jQuery. The first thing we will do is implementing 2 important functions: getHighlightedText() and setHighlightedText(). They return the text in the textarea that's highlighted by the user and replace the highlighted text respectively. The getHighlightedText() function is implemented in this way:

// Returns the text highlighted by the user
function getHighlightedText(){
    var textarea = document.getElementById("description");
    if(typeof textarea.selectionStart == 'number' && typeof textarea.selectionEnd == 'number') {
        // All browsers except IE
        var start = textarea.selectionStart;
        var end = textarea.selectionEnd;          
        return textarea.value.slice(start, end);
    }
}

This function works in all major browsers except Internet Explorer. We won't be supporting IE because it's getting to old and is allready be replaced by Edge. Every textarea object has the selectionStart and selectionEnd properties which indicate the start and end positions of the text that's selected by the user. By taking the value of the textarea and slicing it by using these start and end value's, we get the highlighted portion of the text in the textarea.

The setHighlightedText() function works in a similar manner and is implemented like this:

// Replace the highlighted text with a given string
function setHighlightedText(replacement) {
    var textarea = document.getElementById("description");
    if(typeof textarea.selectionStart == 'number' && typeof textarea.selectionEnd == 'number') {
        // All browsers except IE
        var start = textarea.selectionStart;
        var end = textarea.selectionEnd;

        var before = textarea.value.slice(0, start);
        var after = textarea.value.slice(end);

        // Concatenate the text before and after the user's selection with the replacement string
        var text = before + replacement + after;
        textarea.value = text;
    }
}

We extract the portion of the text before the selected part and after the selected part and then concatenate these with the argument string. At this moment we're going to implement listeners for each button. A listener is, in this case, a function that's called whenever the user clicks on it's associated button. The listener for the bold button works by first taking the text selected by the user and then adding 2 stars at the beginning and the end of the selected text. Text is bold in MarkDown when it starts with 2 stars and ends with 2 stars. To register a function as a listener for clicks in JavaScript you need to use the addEventListener() function:

var boldButton = document.getElementById("bold-button");
boldButton.addEventListener('click', function(){
    var selected = getHighlightedText();
    setHighlightedText("**" + selected + "**");
});

This is still a very basic way to make text bold. Some problems arise with this function. When a user selects some text that's already bold and then presses the bold button again, it will be made bold again (thus it will be preceded by 4 stars). That's not very efficient and clean! We can do better. First of all, we're going to test the highlighted string. If it already starts and ends with 2 stars, we delete these and put the original string back (and thus removing the bold). This way, the button acts as a toggle which looks and feels more natural. The previous function changes to this:

var boldButton = document.getElementById("bold-button");
boldButton.addEventListener('click', function(){
    var selected = getHighlightedText();
    if (selected.startsWith("**") && selected.endsWith("**")){
        setHighlightedText(selected.slice(2, selected.length - 2));
    } else {
        setHighlightedText("**" + selected + "**");
    }
});

Note that the startsWith() and endsWith() functions were introduced with EcmaScript 6 and only work in modern browsers (The newest version of Chrome, Firefox, Opera, Edge and Safari support it).

Implementations for the italic and the header button are quite similar, but the 2 stars are replaced by one underscore at each end of the string in case of italic text and one hash at the beginning of the string in case of a header. This is the implementation:

var italicButton = document.getElementById("italic-button");
italicButton.addEventListener('click', function(){
    var selected = getHighlightedText();
    if (selected.startsWith("_") && selected.endsWith("_")){
        setHighlightedText(selected.slice(1, selected.length - 1));
    } else {
        setHighlightedText("_" + selected + "_");
    }
});

var headerButton = document.getElementById("h1-button");
headerButton.addEventListener('click', function(){
    var selected = getHighlightedText();
    if (selected.startsWith("#")){
        setHighlightedText(selected.slice(1, selected.length));
    } else {
        setHighlightedText("#" + selected);
   }
});

Conclusion

Done! That's it. You are now able to expand this implementation with extra buttons to enable more advanced functions. I've made it possible to insert links and programming code too, but I might explain that in a later post. You're free to copy and use this code as much as you want, but please use credit where appropriate. The full source code is given here:

HTML:


    

How do I use MarkDown to style text?

CSS:

.button-text {
    display: inline-block;
    font-weight: 700;
    font-family: "Palatino Linotype", "Book Antiqua", Palatino, serif;
}

.bottom-padded {
    padding-bottom: 5px;
}

JavaScript:

// Returns the text highlighted by the user
function getHighlightedText(){
    var textarea = document.getElementById("description");
    if(typeof textarea.selectionStart == 'number' && typeof textarea.selectionEnd == 'number') {
        // All browsers except IE
        var start = textarea.selectionStart;
        var end = textarea.selectionEnd;          
        return textarea.value.slice(start, end);
    }
}

// Replace the highlighted text with a given string
function setHighlightedText(replacement) {
    var textarea = document.getElementById("description");
    if(typeof textarea.selectionStart == 'number' && typeof textarea.selectionEnd == 'number') {
        // All browsers except IE
        var start = textarea.selectionStart;
        var end = textarea.selectionEnd;

        var before = textarea.value.slice(0, start);
        var after = textarea.value.slice(end);

        // Concatenate the text before and after the user's selection with the replacement string
        var text = before + replacement + after;
        textarea.value = text;
    }
}

var boldButton = document.getElementById("bold-button");
boldButton.addEventListener("click", function(){
    var selected = getHighlightedText();
    if (selected.startsWith("**") && selected.endsWith("**")){
        setHighlightedText(selected.slice(2, selected.length - 2));
    } else {
        setHighlightedText("**" + selected + "**");
    }
});

var italicButton = document.getElementById("italic-button");
italicButton.addEventListener("click", function(){
    var selected = getHighlightedText();
    if (selected.startsWith("_") && selected.endsWith("_")){
        setHighlightedText(selected.slice(1, selected.length - 1));
    } else {
        setHighlightedText("_" + selected + "_");
    }
});

var headerButton = document.getElementById("h1-button");
headerButton.addEventListener("click", function(){
    var selected = getHighlightedText();
    if (selected.startsWith("#")){
        setHighlightedText(selected.slice(1, selected.length));
    } else {
        setHighlightedText("#" + selected);
   }
});

Post created by Pieter Verschaffelt on 2016-07-03 11:38:42