Banana Code
Banana Code

Reputation: 817

How to prevent the caret jumping to the end of the whole text in contenteditable div after pasting the HTML words in the middle of the original text?

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?

Upvotes: 3

Views: 1669

Answers (1)

Tim Down
Tim Down

Reputation: 324567

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>

Upvotes: 2

Related Questions