1

I just learned how to use selection and range, and built a better way to press enter in contenteditable divs. When enter is pressed, event.preventDefault() is called, and the program inserts <br> where contenteditable would usually add a div. However, I need to press enter twice for anything to happen. Another odd fact: this doesn't happen when there is no text on that line. I am new to selection and range, and I'm sure it's a rookie mistake. Here is the code:

 function keyExamine(e) {
    var doc = document.getElementById('cmd');
    e.keyCode = e.keyCode || e.which;
    if (e.keyCode == 13) {
        if (e.ctrlKey) {
            alert(doc.innerHTML);
        } else {
            e.preventDefault();
            initiateCommand(doc.innerHTML);
            var dv = doc.ownerDocument.defaultView;
            var sel = dv.getSelection();
            var range = sel.getRangeAt(0);
            var enterNode = document.createElement('br');
            range.insertNode(enterNode);
            range.setStartAfter(enterNode);
            range.setEndAfter(enterNode); 
            sel.removeAllRanges();
            sel.addRange(range);
            document.getElementById('maxback').innerText = doc.innerText.replace(/(\r\n\t|\n|\r\t)/gm,"").length;
        }
    } else if (e.keyCode == 38) {
        e.preventDefault();
    } else if (e.keyCode == 37 || e.keyCode == 8 || e.keyCode == 46) {
        var ml = Number(document.getElementById('maxback').innerText);
        if (getCaretPosition(doc) <= ml) {
            e.preventDefault();
        }
    } else {
        var ml = Number(document.getElementById('maxback').innerText);
        if (getCaretPosition(doc) < ml) {
            e.preventDefault();
        }
    }
}

function getCaretPosition(element) {
    var caretOffset = 0;
    var doc = element.ownerDocument || element.document;
    var win = doc.defaultView || doc.parentWindow;
    var sel;
    if (typeof win.getSelection != "undefined") {
        sel = win.getSelection();
        if (sel.rangeCount > 0) {
            var range = win.getSelection().getRangeAt(0);
            var preCaretRange = range.cloneRange();
            preCaretRange.selectNodeContents(element);
            preCaretRange.setEnd(range.endContainer, range.endOffset);
            caretOffset = preCaretRange.toString().length;
        }
    } else if ( (sel = doc.selection) && sel.type != "Control") {
        var textRange = sel.createRange();
        var preCaretTextRange = doc.body.createTextRange();
        preCaretTextRange.moveToElementText(element);
        preCaretTextRange.setEndPoint("EndToEnd", textRange);
        caretOffset = preCaretTextRange.text.length;
    }
    return caretOffset;
}

I have the following HTML:

<p id='maxback' style='display: none'>0</p>
<p contenteditable="true" onkeydown="keyExamine(event)" id="cmd"></p>
Radvylf Programs
  • 1,429
  • 1
  • 12
  • 31
  • [Made it into a jsbin](https://jsbin.com/bowoginezi/edit?js,output) :) (btw notice the log statement and the different values reflected on first ENTER press vs second. Not sure what it means but it was interesting). If we *inspect* the `#cmd` element we actually find that `keyExamine` is reliably inserting the `br` tag! But indeed the first ENTER doesn't display the new line. Weird! [Possible workaround mentioned in this thread](https://stackoverflow.com/questions/15008205/br-not-causing-new-line-on-chrome) – uncleoptimus Apr 07 '18 at 00:59
  • It actually _does_ add the `br`? Odd. Let me try the ` ` trick mentioned in the linked post. – Radvylf Programs Apr 09 '18 at 13:28
  • Okay, now I'm confused. I did the ` ` trick, and it works. Except for the first time you press enter! – Radvylf Programs Apr 09 '18 at 13:37
  • OK just to spitball, the console log shows that when there is text on that line, the references indicate a "text" node. The `br` tag would be inserted but the newline would not be displayed. If it was a blank line, it would show the expected "p#cmd" node that I think we want. – uncleoptimus Apr 09 '18 at 23:27
  • So in other words seems like that selector is finding the wrong result if there is text on the line. Hitting "enter" once appends a `br` tag and so I guess the next time the selector runs (hitting "enter" the second time) that allows it to find our expected target reference and boom we got our newline. If that ref is wrong, ie the "sel" variable, that explains why adding the range to the "text node" instead of the "p#cmd" element didn't result in what we wanted. Not sure why that happens. Glad the "&nbsp" trick works tho! – uncleoptimus Apr 09 '18 at 23:27

1 Answers1

1

There exists a way to handle this that takes care of all the issues while avoiding numerous pitfalls. You can insert a text node containing a newline, and then collapse all selections to the end of it.

This method is both platform- and browser-independent, working all the way back to IE9 (in theory).

The major downside? It relies on undefined behavior, setting the collapse index beyond the node length on purpose. It just so happens that all browsers correctly set the selection after the inserted node, even on the very first Enter press.

function handleKey(event) {
  const editor = document.getElementById('cmd')
  if (event.key === 'Enter') {
    const node = document.createTextNode('\n')
    const range = window.getSelection().getRangeAt(0)
    range.insertNode(node)
    
    const selection = document.getSelection()
    if (selection) {
      selection.collapse(node, 2)
    }
    
    event.preventDefault()
  }
}

Looking forward to browser vendors making this the default behavior when collapse is called with index 1 as well. Hopefully that happens someday.

mmKALLL
  • 154
  • 3
  • 9