Search code examples
javascriptjqueryhtmlnestedcontenteditable

contenteditable div - divide a span tag - avoid nesting


I'm working on a typing tool using contenteditable. When the user types some text I insert it in a span tag and add it to the main div; the result is something like this:

<span style="color: black;">hello</span>

If the user puts the caret inside the word "hello" and starts typing with a different color the result will be:

<span style="color: black;">he<span style="color: red;">new text</span>llo</span>

What I actually would like to achieve is:

<span style="color: black;">he</span>
<span style="color: red;">new text</span>
<span style="color: black;">llo</span>

I would like to avoid having nested span elements. Currently I use this procedure to add the span tag:

var sel = window.getSelection();
var range = sel.getRangeAt(0);

var spanTag = "<span id='newSpan' style='color: " + currentColor + "'>00000</span>"; // &#8203;
var documentFragment = range.createContextualFragment(spanTag);

range.insertNode(documentFragment);

var dummySpan = document.getElementById("newSpan");

range.setStart(dummySpan, 1);
range.setEnd(dummySpan, 1);

sel.removeAllRanges();
sel.addRange(range);

$("#newSpan").removeAttr("id");
dummySpan.innerHTML = "";

I'm just wondering if I'm missing some jquery or javascript function that could help to achieve this easily.


Solution

  • Finally I made it with this function. Basically I split in half the current tag text at the caret position, create two separate tags each with half of the text, create a new tag with the current color in the middle and put the caret in it.

    function typeInKanji() {
    
        var sel = window.getSelection();
        var range = sel.getRangeAt(0);
    
        var currentNode = sel.anchorNode;   
        var elementNode = getSelectionElement();
    
        var caretPosition = getCaretCharacterOffsetWithin(elementNode);
    
        if (elementNode.tagName == "SPAN") { 
    
            if (elementNode.style.color == currentColor) {
    
                console.log("span of same color, do nothing and keep typing");
    
            } else {
    
                console.log("span of a different color, split it");
    
                var tagText = elementNode.innerText;
    
                console.log("text: " + tagText + " position: " + caretPosition);
    
                var firstHalf = tagText.substr(0, caretPosition);
                var secondHalf = tagText.replace(firstHalf, "");
    
                // remove the old element with all the text
                $(elementNode).remove();
    
                var firstTag = "<span style='color: " + elementNode.style.color + "'>" + firstHalf + "</span>"
                var secondTag = "<span style='color: " + elementNode.style.color + "'>" + secondHalf + "</span>"
                var middleTag = "<span id='middleTag' style='color: " + currentColor + "'>00</span>"
    
                var firstFrag = range.createContextualFragment(firstTag);
                var secondFrag = range.createContextualFragment(secondTag);
                var middleFrag = range.createContextualFragment(middleTag);
    
                range.insertNode(secondFrag);
                range.insertNode(middleFrag);
                range.insertNode(firstFrag);
    
                var getMiddleTag = document.getElementById("middleTag");
    
                console.log(getMiddleTag);
    
                range.setStart(getMiddleTag, 0);
                range.setEnd(getMiddleTag, 0);
    
                sel.removeAllRanges();
                sel.addRange(range);
    
                $("#middleTag").removeAttr("id");
                getMiddleTag.innerHTML = "";
    
            }
    
    
        } else {
    
            // if there is not SPAN tag create one
            insertTag("<span id='newSpan' style='color: " + currentColor + "'>00</span>");
    
            var dummySpan = document.getElementById("newSpan");
    
            range.setStart(dummySpan, 1);
            range.setEnd(dummySpan, 1);
    
            sel.removeAllRanges();
            sel.addRange(range);
    
            $("#newSpan").removeAttr("id");
            dummySpan.innerHTML = "";
    
        }
    
        typeKanjiNow = false;
    
    }
    

    And here the other two functions I use:

    function getCaretCharacterOffsetWithin(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;
    
    }
    
    function insertTag(tag) {
    
        var sel = window.getSelection();
        var range = sel.getRangeAt(0);
    
        var currentNode = sel.anchorNode;   
    
        var documentFragment = range.createContextualFragment(tag);
        range.insertNode(documentFragment);
    
    }