views:

130

answers:

1

I have the following html:

<html>
 <head>
   <script>

function myKeyDown()
{
    var myDiv = document.getElementById('myDiv');
    myDiv.innerHTML = myDiv.innerHTML.replace(/(@[a-z0-9_]+)/gi, '<strong>$1</strong>');
}

function init()
{
    document.addEventListener('keydown', myKeyDown, false);
    document.designMode = "on";
}

window.onload = init;
   </script>
 </head>
 <body>
   <div id="myDiv">
     This is my variable name: @varname. If I type here things go wrong...
   </div>
 </body>
</html>

My goal is to do a kind of syntax highlighting on edit, to highlight variable names that begin with an @ symbol. However, when I edit the document's body, the function runs but the cursor is automatically shifted to the beginning of the body before the keystroke is performed.

My hypothesis is that the keypress event is trying to insert the new character at a specified index, but when I run the replace function the indices get messed up so it defaults the character insertion point to the beginning.

I'm using Firefox to test by the way.

Any help would be appreciated.

Thanks, B.J.

+1  A: 

Your hypothesis is correct. Doing the replacement using innerHTML means that the browser has to throw away all the nodes inside your div and create new ones from the HTML string you provide, meaning the nodes the selection previously existed in no longer exist. It's very inefficient, particularly to be doing this on every keypress. You need to do this using DOM methods instead.

I'd suggest waiting for a period of keyboard inactivity before doing any substitutions rather than doing it after every keypress.

Finally, which browsers does this need to work in?

UPDATE

I decided this was an interesting problem, so I've written a full solution. I also changed my mind about using innerHTML. The complicated bit is the completely different method you have to use for saving and restoring the selection in IE from all other browsers. I also changed it to use contenteditable rather than designMode. This rules out Firefox 2, but it simplified some things so I hope that's OK.

<script type="text/javascript">
function getBoundary(el, textNodes, charIndex) {
    var charsSoFar = 0, textNodeLength;

    // Walk text nodes
    for (var i = 0, len = textNodes.length; i < len; ++i) {
        textNodeLength = textNodes[i].data.length;
        charsSoFar += textNodeLength;
        if (charsSoFar >= charIndex) {
            return {
               node: textNodes[i],
               offset: charIndex + textNodeLength - charsSoFar
            };
        }
    }

    throw new Error("Boundary not found");
}

function highlightVars() {
    var myDiv = document.getElementById('myDiv');
    var selectedRange, range, divText;
    var selectionStartPos, selectionLength;

    var hasRanges = !!(window.getSelection
        && document.createRange);
    var hasTextRanges = !!(document.selection
        && document.body.createTextRange);

    // Get the selection text position within the div
    if (hasRanges) {
        selectedRange = window.getSelection().getRangeAt(0);
        range = document.createRange();
        range.selectNodeContents(myDiv);
        divText = range.toString();
        range.setEnd(selectedRange.startContainer, selectedRange.startOffset);
        selectionStartPos = range.toString().length;
        selectionLength = selectedRange.toString().length;
    } else if (hasTextRanges) {
        selectedRange = document.selection.createRange();
        range = document.body.createTextRange();
        range.moveToElementText(myDiv);
        divText = range.text;
        range.setEndPoint("EndToStart", selectedRange);
        selectionStartPos = range.text.length;
        selectionLength = selectedRange.text.length;
    }

    // Substitute in existing text with vars highlighted
    myDiv.innerHTML = divText.replace(/(@[a-z0-9_]+)/gi,
       '<strong>$1</strong>').replace(/ $/, "\u00a0");

    // Restore selection
    if (hasRanges) {
        var textNodes = [];

        for (var n = myDiv.firstChild; n; n = n.nextSibling) {
            textNodes.push( (n.nodeType == 1) ? n.firstChild : n );
        }

        var selectionStartBoundary = getBoundary(myDiv, textNodes,
            selectionStartPos);
        var selectionEndBoundary = getBoundary(myDiv, textNodes,
            selectionStartPos + selectionLength);

        range.setStart(selectionStartBoundary.node,
            selectionStartBoundary.offset);
        range.setEnd(selectionEndBoundary.node,
            selectionEndBoundary.offset);

        var sel = window.getSelection();
        sel.removeAllRanges();
        sel.addRange(range);
    } else if (hasTextRanges) {
        range.moveToElementText(myDiv);
        range.moveStart("Character", selectionStartPos);
        range.collapse();
        range.moveEnd("Character", selectionLength);
        range.select();
    }
}

var keyTimer;

function myKeyDown() {
    if (keyTimer) {
        window.clearTimeout(keyTimer);
    }
    keyTimer = window.setTimeout(function() {
        keyTimer = null;
        highlightVars();
    }, 1000);
}

function init() {
    var myDiv = document.getElementById("myDiv");
    myDiv.onkeydown = myKeyDown;
}

window.onload = init;
</script>

<body>
   <div id="myDiv" contenteditable="true">This is my variable
   name: @varname. If I type here things go wrong...</div>
</body>
Tim Down
I need this to work for at least IE and Firefox, but all-browser compatibility would be best.I'm not sure what you mean by using DOM methods instead though. I need this to be a type of syntax highlighting, meaning changing colors or whatever immediately after the user types a particular word.
Benny
I've rewritten my answer.
Tim Down
That is a very impressive piece of code. It works perfectly with one minor exception: If you type a string of text with an additional space at the end and then type another character it overwrites the whitespace at the end with the new character. I think it has something to do with the document.selection not including trailing whitespace. Any ideas?
Benny
It's to do with Firefox's implementation of `innerHTML`, I think. I've changed it so that any trailing space is replaced with a non-breaking space and the problem is fixed.
Tim Down
One other problem I've found has to do with putting in line breaks. If you insert a line break at the end of the string it works, but if you try to add anything after that line break, it thinks the cursor is at the end of the line before the break.
Benny
Hmm. This kind of problem is why I recommended not using `innerHTML` in the first place. I'll have a play.
Tim Down
This is a non-trivial task. My solution is hopeless with line breaks, because I wrote it as quickly as possible. I think it might be better to do it by walking the DOM, scanning text nodes for variables and surrounding them in `<strong>` elements, and examining the contents of each existing `<strong>` element to check if it still contains a variable. I don't think I have time to write this for you.
Tim Down