There are nice web editors out there which are ready to be used: you just download them and plug them in your page. I have used myself both CodeMirror and ACE in the past. For example I wrote a plugin for CodeMirror to support PlantUML. However there is an issue with these editors: they are difficult to extend and difficult to understand.

When I look at the code of these products I see something that I cannot easily understand, something I do not feel confident building upon.

Now, my philosophy is to build simple tools, that work, can be understood, combined and extended. So I wanted to try another approach, building my own simple web editor from scratch.

js web editor

Following the rule of putting your code where your mouth is, here comes the GitHub repo: https://github.com/ftomassetti/simple-web-editor

HTML

Let’s start slow, with some HTML:

<html>
<head>
    <link rel="stylesheet" type="text/css" href="css/style.css" media="screen" />
    <script src="js/jquery-3.1.1.min.js"></script>
    <script src="js/webeditor.js"></script>
    <link href="https://fonts.googleapis.com/css?family=Source+Code+Pro" rel="stylesheet">
</head>
<body>
    <h1>My Simple Web Editor</h1>
    <div id="editor">        
    </div>
    <span class="blinking-cursor">|</span>    
<body>    
</html>

What we have here?

  • jquery, of course
  • some CSS
  • a cool font from Google
  • a JS file with all of our code (wededitor.js)
  • a div (editor) and a span for our blinking editor

TypeScript

Now, we are going to use TypeScript with the hope that it can reduce a little bit the pain of using JavaScript. And also because I want to try it out. For people who have never used it before TypeScript is basically a superset of JavaScript which permits to optionally specify types. Types are used to check for errors and then forgot because in the end we generate JavaScript. You can use JavaScript libraries in TypeScript and when you want to do that you may want to import a description of all the types in that library. This is what we import in the first line.

/// <reference path="defs/jquery.d.ts" />

class Editor {
    private caretIndex: number;
    private text: string;

    constructor() {
        this.caretIndex = 0;
        this.text = "";
    }

    textBeforeCaret() {
        if (this.caretIndex == 0) {
            return "";
        } else {
            return this.text.substring(0, this.caretIndex);
        }
    }

    textAfterCaret() {
        if (this.caretIndex  == this.text.length) {
            return "";
        } else {
            return this.text.substring(this.caretIndex );
        }
    }

    generateHtml() {
        return this.textBeforeCaret() 
                + "<span class='cursor-placeholder'>|</span>"
                + this.textAfterCaret();
    }

    type(c:string) {
        this.text = this.textBeforeCaret() + c + this.textAfterCaret();
        this.caretIndex = this.caretIndex + 1;
    }

    deleteChar() : boolean {
        if (this.textBeforeCaret().length > 0) {
            this.text = this.textBeforeCaret().substring(0, this.textBeforeCaret().length - 1) + this.textAfterCaret();
            this.caretIndex--;
            return true;
        } else {
            return false;
        }
    }

    moveLeft() : boolean {
        if (this.caretIndex == 0) {
            return false;
        } else {
            this.caretIndex--;
            return true;
        }
    }

    moveRight() : boolean {
        if (this.caretIndex == this.text.length) {
            return false;
        } else {
            this.caretIndex++;
            return true;
        }
    }    
}

var updateHtml = function() {   
    $("#editor")[0].innerHTML = (window as any).editor.generateHtml();
    var cursorPos = $(".cursor-placeholder").position();
    var delta = $(".cursor-placeholder").height() / 4.0;
    $(".blinking-cursor").css({top: cursorPos.top, left: cursorPos.left - delta});        
};

$( document ).ready(function() {        
    (window as any).editor = new Editor();

    updateHtml();
    $(document).keypress(function(e){
        var c = String.fromCharCode(e.which);   
        (window as any).editor.type(c);        
        updateHtml();
    });
    $(document).keydown(function(e){
        if (e.which == 8 && (window as any).editor.deleteChar()) {            
            updateHtml();
        };
        if (e.which == 37 && (window as any).editor.moveLeft()) {
            updateHtml();
        };
        if (e.which == 39 && (window as any).editor.moveRight()) {
            updateHtml();
        };
    });
});

Ok, let’s look at the code now. We have:

  • the Editor class
  • the function updateHTML
  • the wiring in $(document).ready(…)

The editor class

The editor class is where we do the heavy lifting. We store two things:

  1. the text contained in the editor
  2. the position of the caret within the text

TextBeforeCaret and TextAfterCaret obviously give us all the text before or after the caret (surprised?).

What generateHTML does? It generate the HTML code for the text placing a span to indicate the position of the caret: this element is the caret placeholder. Why we do not put the caret itself? Because the caret has a size, so if we would move it around inside the text we would cause all the text to move all the time. Instead we move the caret placeholder, which has zero size and then we use place the caret above the caret placeholder but at a different z-index. In this way we basically see the caret where we want to see it without moving the text left or right to leave place for the caret.

The remaining methods permit to:

  • insert a character
  • delete a character
  • move the caret left
  • move the caret right

The function updateHTML

The function updateHTML implement the trick we have seen for the caret:

var updateHtml = function() {   
    $("#editor")[0].innerHTML = (window as any).editor.generateHtml();
    var cursorPos = $(".cursor-placeholder").position();
    var delta = $(".cursor-placeholder").height() / 4.0;
    $(".blinking-cursor").css({top: cursorPos.top, left: cursorPos.left - delta});        
};

First we update the content of the editor, then we find the position of the caret placeholder and then we move the blinking-cursor (a.k.a. the caret) just above the placeholder. We actually move it a little bit on the left because in this way it looks nicer.

The wiring

The wiring consist in attaching event handler to:

  • get when we type a character
  • get when we delete a character
  • get when we use the left and right arrow

We then just call methods from the Editor class.

Conclusions

Ok, we started with something simple: a very minimal editor where we can type, delete, and moving around using the arrow. This is not the most impressive editor ever seen. But it is simple and it works. We can build on top of that something sensible, that does what we need and it still understandable and extensible.