3

Summary/Background

I'm trying to make a contenteditable div and when it was pasted by the HTML words, I hope that it could be transferred into plain text and the caret could jump to the end of the pasted HTML words automatically.

My try

I have tried to make it, and below is my code I have typed.

<html>

<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Test</title>
</head>

<body>
<script src="http://code.jquery.com/jquery-1.11.3.min.js"></script>
<script>
function stripHTML(input){
        var output = '';
        if(typeof(input) == "string"){
                output = input.replace(/(<([^>]+)>)/ig, "");
        }
        return output;
}

$(function(){
        $("#text").focus(function(event){
                setItv = setInterval('clearHTML()', 1);
        });
        $("#text").blur(function(event){
                clearInterval(setItv);
        });
});

function clearHTML(){
        var patt = /(<([^>]+)>)/ig;
        var input = $("#text").html();

        if(input.search(patt) >= 0){
                var output = '';
                if(typeof(input) == "string"){
                        output = stripHTML(input);
                }
                $("#text").html(output);
        }
}
</script>

<div contenteditable="true" style="border: 1px #000 solid; width: 300px;
       height: 100px;" id="text">Hello, world!</div>

</body>

</html>

Issue

The biggest issue is the cursor placement. After pasting the HTML in the middle of the original text (e.g., paste between “Hello” and “world”), the caret would jump to the end of the whole text not the end of the pasted HTML. Normally, when we paste a snippet of words in the middle of the original text, the caret would jump to the end of the pasted words, not the end of the whole text. But in this case I don't know why it jumps to the end of the whole text automatically.

And the secondary issue is that the setInterval function. Maybe it doesn't cause any problem, but the way of scripting is extremely unprofessional and it may impact the efficiency of the program.

Question

  1. How to prevent the caret from jumping to the end of the whole text in contenteditable div after pasting the HTML words in the middle of the original text?
  2. How to optimize the scripting without using the setInterval function?
Banana Code
  • 759
  • 1
  • 12
  • 28

1 Answers1

2

As a starting point, I would suggest the following improvements:

  • Check directly for the presence of elements in your editable content rather than using a regular expression on the innerHTML property. This will improve performance because innerHTML is slow. You could do this using the standard DOM method getElementsByTagName(). If you prefer to use jQuery, which will be slightly less efficient, you could use $("#text").find("*"). Even simpler and faster would be to check that your editable element has a single child which is a text node.
  • Strip the HTML by replacing the element's content with a text node containing the element's textContent property. This will more reliable than replacing HTML tags with a regular expression.
  • Store the selection as character offsets before stripping HTML tags and restore it afterwards. I have previously posted code on Stack Overflow to do this.
  • Do the HTML stripping when relevant events fire (I suggest keypress, keyup, input and paste for starters). I'd keep the setInterval() with a less frequent interval to deal with other cases not covered by those events

The following snippet contains all these improvements:

var saveSelection, restoreSelection;

if (window.getSelection && document.createRange) {
    saveSelection = function(containerEl) {
        var range = window.getSelection().getRangeAt(0);
        var preSelectionRange = range.cloneRange();
        preSelectionRange.selectNodeContents(containerEl);
        preSelectionRange.setEnd(range.startContainer, range.startOffset);
        var start = preSelectionRange.toString().length;

        return {
            start: start,
            end: start + range.toString().length
        };
    };

    restoreSelection = function(containerEl, savedSel) {
        var charIndex = 0, range = document.createRange();
        range.setStart(containerEl, 0);
        range.collapse(true);
        var nodeStack = [containerEl], node, foundStart = false, stop = false;

        while (!stop && (node = nodeStack.pop())) {
            if (node.nodeType == 3) {
                var nextCharIndex = charIndex + node.length;
                if (!foundStart && savedSel.start >= charIndex && savedSel.start <= nextCharIndex) {
                    range.setStart(node, savedSel.start - charIndex);
                    foundStart = true;
                }
                if (foundStart && savedSel.end >= charIndex && savedSel.end <= nextCharIndex) {
                    range.setEnd(node, savedSel.end - charIndex);
                    stop = true;
                }
                charIndex = nextCharIndex;
            } else {
                var i = node.childNodes.length;
                while (i--) {
                    nodeStack.push(node.childNodes[i]);
                }
            }
        }

        var sel = window.getSelection();
        sel.removeAllRanges();
        sel.addRange(range);
    }
} else if (document.selection) {
    saveSelection = function(containerEl) {
        var selectedTextRange = document.selection.createRange();
        var preSelectionTextRange = document.body.createTextRange();
        preSelectionTextRange.moveToElementText(containerEl);
        preSelectionTextRange.setEndPoint("EndToStart", selectedTextRange);
        var start = preSelectionTextRange.text.length;

        return {
            start: start,
            end: start + selectedTextRange.text.length
        }
    };

    restoreSelection = function(containerEl, savedSel) {
        var textRange = document.body.createTextRange();
        textRange.moveToElementText(containerEl);
        textRange.collapse(true);
        textRange.moveEnd("character", savedSel.end);
        textRange.moveStart("character", savedSel.start);
        textRange.select();
    };
}

$(function(){
  var setInv;
  var $text = $("#text");

  $text.focus(function(event){
    setItv = setInterval(clearHTML, 200);
  });
  
  $text.blur(function(event){
    clearInterval(setItv);
  });
  
  $text.on("paste keypress keyup input", function() {
    // Allow a short delay so that paste and keypress events have completed their default action bewfore stripping HTML
    setTimeout(clearHTML, 1)
  });
});

function clearHTML() {
  var $el = $("#text");
  var el = $el[0];
  if (el.childNodes.length != 1 || el.firstChild.nodeType != 3 /* Text node*/) {
    var savedSel = saveSelection(el);
    $el.text( $el.text() );
    restoreSelection(el, savedSel);
  }
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<div contenteditable="true" style="border: 1px #000 solid; width: 300px;
       height: 100px;" id="text">Hello, world!</div>
Community
  • 1
  • 1
Tim Down
  • 318,141
  • 75
  • 454
  • 536
  • Pardon, if now I allow the images (only tags ) appear in the contenteditable div, and how should I modify the code? – Banana Code May 17 '15 at 16:56
  • @RedWhale: That's quite a different and more complicated problem. You'll need to go through the content of the editable element and remove elements except `` elements, which I would do recursively. Otherwise the approach is the same. – Tim Down May 18 '15 at 08:51
  • Thank you so much. But there's still a crux that didn't answer... Is there any better way to optimize the program without using the setInterval function or setTimeout function? – Banana Code May 18 '15 at 13:41
  • @RedWhale: I touched on that in my answer. I don't think you can avoid them entirely. You need `setTimeout()` for the `paste` event, for example, because it fires before the paste has actually happened. – Tim Down May 18 '15 at 15:55
  • I found the other 2 problems in IE: Why in IE11 it doesn't strip the HTML tags anymore? And in IE 8 the caret jumps to the rear 2 characters of the pasted words after pasting the HTML words which contains the `
    ` tag.
    – Banana Code May 19 '15 at 01:42
  • @RedWhale: Works fine for me in IE 11. In IE 8, which does not support the same selection and range API as other browsers, line breaks can cause an issue with the save/restore selection code. It's complicated to work round. – Tim Down May 19 '15 at 09:14
  • But if you save your code (above) as an html file in your computer's desktop, open it with IE11, paste the words which contain `
    ` tag in the contenteditable div box. Then you might see the error. I don't know why the contenteditable div in my IE11 acted wrongly and didn't strip the HTML tags. Is my computer's IE11 broken?
    – Banana Code May 19 '15 at 10:55
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/78181/discussion-between-red-whale-and-tim-down). – Banana Code May 19 '15 at 11:38