JSFiddle - React, Tailwind, and code Playground

by Valentin Sarychev

HTML

<div class="editable" contenteditable="true"><p>paragraph one</p><p>paragraph two</p></div>

<hr/>

<p>How to reproduce the bug:</p>
<ul>http://jsfiddle.net/dizel3d/sz5qnncp/15/#set-as-base
    <li>Focus the contenteditable above by placing the cursor somewhere in one of the two paragraphs.</li>
    <li>press ctrl-a (in windows or linux) or cmd-a (in osx) to select-all</li>
    <li>type some text</li>
    <li>red text means that the text went directly inside the contenteditable div, black text means it went inside a paragraph</li>
</ul>

<p>The correct behaviour should be that that select-all and delete (or "type-over") in a contenteditable with only block tags should leave the cursor inside the first block tag.</p>

<p>Webkit gets this right, Firefox gets it wrong.</p>

CSS

body {
    font-family: georgia;
}
.editable {
    color: red;
}
.editable p {
    color: #333;
}
.editable span {
    color: limegreen !important;
}

JavaScript

// Handler decorator, which passes "delete text" events only
function proxyDelete(func) {
    return function(e) {
        // skip all keys except DELETE and BACKSPACE
        if (e.keyCode !== 8 & e.keyCode !== 46) {
            return;
        }
        
        return func.apply(this, arguments);
    };
}

function textNodesUnder(el){
    var n, a = [], walk = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null, false);
    while ((n = walk.nextNode())) a.push(n);
    return a;
}

jQuery.fn.htmlSplittedBySelection = function() {
    var selection = window.getSelection();
    var textNodes = textNodesUnder(this[0]);
    var range = [];
    var pos = 0;
    
    for (var i = 0; i < textNodes.length; ++i) {
        var node = textNodes[i];
        if (selection.baseNode === node) {
            range.push(pos + selection.baseOffset);
        }
        if (selection.extentNode === node) {
            range.push(pos + selection.extentOffset);
        }
        pos += node.length;
    }
    range.sort();
    
    var html = this.html();
    var result = [];
    var add = true;
    var start = 0;
    pos = 0;
    
    console.log(range);
    
    for (var end = 0; end < html.length; ++end) {
        if (html[end] === '<') {
            add = false;
        } else if (html[end] === '>') {
            add = true;
        } else if (add) {
            if (pos === range[0]) {
                result.push(html.substring(start, end));
                start = end;
                range.shift();
            }
            ++pos;
        }
        if (range.length === 0) {
            result.push(html.substring(end));
            break;
        }
    }
    return result;
};

$(document).on('keydown', '[contenteditable]', proxyDelete(function(e) {
    var $this = $(this);

    console.log($this.htmlSplittedBySelection());
    
    // if DELETE
    if (e.keyCOde === 8) {
            
        return;
    }
    
    // if BACKSPACE
    if (e.keyCOde === 46) {
        
       ...