Noctisdark
Noctisdark

Reputation: 348

Syntax highlight with JavaScript

I'm trying to make a simple syntax highlighter with JavaScript, but I always end up having the same problem. The program works as follows: as the user types enter (without the shift key) the program will replace the keyword var with another one with red color (this is still so basic). The problem is that whenever you press enter, the text gets highlighted but the cursor returns to the first word of the first line. How do you think I can prevent this from happening?

<div class="container">
    <pre class="text"><code contenteditable="true" id="format">
    </code></pre>
</div>

JS

var editor = document.getElementById('format');
var npatt = / *var +/igm
editor.addEventListener('keyup', highlight);

function highlight(e){
    var content = editor.innerHTML;
    if(e.which === 13 && e.shiftKey===false){
        editor.innerHTML = content.replace(npatt, '<span style="color:red">var</span>&nbsp;');
        console.log(editor.innerHTML);
    }
}

Upvotes: 1

Views: 984

Answers (1)

Drew Gaynor
Drew Gaynor

Reputation: 8482

Moving the cursor to the end of a contenteditable element can be done according to the method in this answer. That approach uses the window.getSelection() method to find the cursor position.

I made a few changes to your code.

  1. Added a test check to see if the regular expression even matches the content to avoid calling replace and setting editor.innerHTML on every Enter keystroke as the original code did.
  2. Added a call to the cursorManager.setEndOfContenteditable method (from the answer referenced above) to reset the cursor to the end of the editor after the replace operation.

Here is the updated code.

var editor = document.getElementById('format');
var npatt = / *var +/igm;

editor.addEventListener('keyup', highlight);

function highlight(e){
    var content = editor.innerHTML;

    if(e.which === 13 && e.shiftKey === false && npatt.test(content)) {
        editor.innerHTML = content.replace(npatt, '<span style="color:red">var</span>&nbsp;');
        cursorManager.setEndOfContenteditable(editor);
    }
}

And here is a working example.

var editor = document.getElementById('format');
var npatt = / *var +/igm;

editor.addEventListener('keyup', highlight);

function highlight(e){
    var content = editor.innerHTML;
  
    if(e.which === 13 && e.shiftKey === false && npatt.test(content)) {
        editor.innerHTML = content.replace(npatt, '<span style="color:red">var</span>&nbsp;');
        cursorManager.setEndOfContenteditable(editor);
    }
}

//Code to set the cursor position modified from this answer: https://stackoverflow.com/a/19588665/830125
//Namespace management idea from http://enterprisejquery.com/2010/10/how-good-c-habits-can-encourage-bad-javascript-habits-part-1/
(function( cursorManager ) {

    //From: http://www.w3.org/TR/html-markup/syntax.html#syntax-elements
    var voidNodeTags = ['AREA', 'BASE', 'BR', 'COL', 'EMBED', 'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK', 'MENUITEM', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR', 'BASEFONT', 'BGSOUND', 'FRAME', 'ISINDEX'];

    //From: https://stackoverflow.com/questions/237104/array-containsobj-in-javascript
    Array.prototype.contains = function(obj) {
        var i = this.length;
        while (i--) {
            if (this[i] === obj) {
                return true;
            }
        }
        return false;
    }

    //Basic idea from: https://stackoverflow.com/questions/19790442/test-if-an-element-can-contain-text
    function canContainText(node) {
        if(node.nodeType == 1) { //is an element node
            return !voidNodeTags.contains(node.nodeName);
        } else { //is not an element node
            return false;
        }
    };

    function getLastChildElement(el){
        var lc = el.lastChild;
        while(lc && lc.nodeType != 1) {
            if(lc.previousSibling)
                lc = lc.previousSibling;
            else
                break;
        }
        return lc;
    }

    //Based on Nico Burns's answer
    cursorManager.setEndOfContenteditable = function(contentEditableElement)
    {
        var range,selection;
        if(document.createRange)//Firefox, Chrome, Opera, Safari, IE 9+
        {    
            range = document.createRange();//Create a range (a range is a like the selection but invisible)
            range.selectNodeContents(contentEditableElement);//Select the entire contents of the element with the range
            range.collapse(false);//collapse the range to the end point. false means collapse to end rather than the start
            selection = window.getSelection();//get the selection object (allows you to change selection)
            selection.removeAllRanges();//remove any selections already made
            selection.addRange(range);//make the range you have just created the visible selection
        }
        else if(document.selection)//IE 8 and lower
        { 
            range = document.body.createTextRange();//Create a range (a range is a like the selection but invisible)
            range.moveToElementText(contentEditableElement);//Select the entire contents of the element with the range
            range.collapse(false);//collapse the range to the end point. false means collapse to end rather than the start
            range.select();//Select the range (make it the visible selection
        }
    }

}( window.cursorManager = window.cursorManager || {}));
<div class="container">
    <pre class="text"><code contenteditable="true" id="format">
    </code></pre>
</div>

Upvotes: 2

Related Questions