Zerotoinfinity
Zerotoinfinity

Reputation: 6540

Replace specific word in contenteditable

I have a contenteditable div

<div id="divTest" contenteditable="true">

I need to get the last word from caret position and on certain condition I have to test and remove this specific word only. Below is how am I doing

$('#divTest').on('keyup focus', function (e) {
           if (e.keyCode == 32) {
               var lastWord = getWordPrecedingCaret(this),                                       spanLastWord = $('#lastWord');

           }
       });

function getWordPrecedingCaret(containerEl) {
               var preceding = "",
                                    sel,
                                    range,
                                    precedingRange;
               if (window.getSelection) {
                   sel = window.getSelection();
                   if (sel.rangeCount > 0) {
                       range = sel.getRangeAt(0).cloneRange();
                       range.collapse(true);
                       range.setStart(containerEl, 0);
                       preceding = range.toString();


                   }
               } else if ((sel = document.selection) && sel.type != "Control") {
                   range = sel.createRange();
                   precedingRange = range.duplicate();
                   precedingRange.moveToElementText(containerEl);
                   precedingRange.setEndPoint("EndToStart", range);
                   preceding = precedingRange.text;
               }

               var words = range.toString().trim().split(' '),
                        lastWord = words[words.length - 1];
                  if (lastWord) {

                   var resultValue = 'some'; // this value is coming from some other function
                   if (resultValue == lastWord) {
                     alert('do nothing');
                       // do nothing
                   }
                 else
                   {
                     alert('replace word');
                     // delete That specific word and replace if with resultValue
                   }
                   return lastWord;

               }
           }

Demo: http://codepen.io/anon/pen/ogzpXV

I have tried range.deleteContents(); but that will delete all the content in the div. How can I replace specific word only?

Upvotes: 12

Views: 6533

Answers (3)

Tob&#237;as
Tob&#237;as

Reputation: 6297

To work with Ranges we need to keep in mind that we are working with Nodes, not only the text that is rendered. The structure you want to manipulate is:

<div id="divTest" contenteditable="true"> <-- Element Node
    "some text" <-- TextNode
</div>

But it also could be:

<div id="divTest" contenteditable="true"> <-- Element Node
    "some text" <-- TextNode
    "more text" <-- TextNode
    "" <-- TextNode
</div>

To solve your problem is simplier to handle only one TextNode, I propose to use the normalize() function to join all of them into a single one.

Then you only need to set the Range to the word's bounds before deleteContents(). Once deleted, you can insert a new TextNode with the substitution using insertNode().

var wordStart = range.toString().lastIndexOf(lastWord);
var wordEnd = wordStart + lastWord.length;

/* containerEl.firstChild refers to the div's TextNode */                   
range.setStart(containerEl.firstChild, wordStart);
range.setEnd(containerEl.firstChild, wordEnd);
range.deleteContents();
range.insertNode(document.createTextNode(resultValue));

For this to work, you need that the text is in a single TextNode. But after ìnsertNode the div will contain multiple text nodes. To fix this simply call normalize() to join all TextNode elements.

containerEl.normalize();

Edit:

As Basj points out, the original solution fails for multiline. That's because when hitting ENTER the structure changes from:

<div id="divTest" contenteditable="true"> <-- Element Node
    "some text" <-- TextNode
</div>

to something like:

<div id="divTest" contenteditable="true"> <-- Element Node
    <div>"some text"</div>
    <div>"more text"</div>
</div>

I've updated this answer, but it's also worth to read Basj's answer at this question: Replace word before cursor, when multiple lines in contenteditable

JSFiddle demo or runnable code snippet:

document.getElementById('divTest').onkeyup = function (e) {
    if (e.keyCode == 32) {
        getWordPrecedingCaret(this);
    }
};

function getWordPrecedingCaret(containerEl) {
    var preceding = "",
        sel,
        range,
        precedingRange;
    if (window.getSelection) {
        sel = window.getSelection();
        if (sel.rangeCount > 0) {
            range = sel.getRangeAt(0).cloneRange();
            range.collapse(true);
            range.setStart(containerEl, 0);
            preceding = range.toString();
        }
    } else if ((sel = document.selection) && sel.type != "Control") {
        range = sel.createRange();
        precedingRange = range.duplicate();
        precedingRange.moveToElementText(containerEl);
        precedingRange.setEndPoint("EndToStart", range);
        preceding = precedingRange.text;
    }

    var words = range.toString().trim().split(' '),
        lastWord = words[words.length - 1];
        
    if (lastWord) {
        var resultValue = 'some'; // this value is coming from some other function
        if (resultValue == lastWord) {
            console.log('do nothing: ' + lastWord);
            // do nothing
        } else {
            console.log('replace word ' + lastWord);
            
            /* Find word start and end */
            var wordStart = range.endContainer.data.lastIndexOf(lastWord);
            var wordEnd = wordStart + lastWord.length;
            console.log("pos: (" + wordStart + ", " + wordEnd + ")");
                           
            range.setStart(range.endContainer, wordStart);
            range.setEnd(range.endContainer, wordEnd);
            range.deleteContents();
            range.insertNode(document.createTextNode(resultValue));
            // delete That specific word and replace if with resultValue

            /* Merge multiple text nodes */            
            containerEl.normalize();
        }
        return lastWord;
    }
}
<div id="divTest" contenteditable="true">Write words here and hit SPACE BAR</div>

Upvotes: 9

Basj
Basj

Reputation: 46563

Tobías' solution works well for single-line contenteditable div. But if you add multiple lines, it doesn't work anymore.

Here is a general solution that works for both single-line or multiline contenteditable div.

Upvotes: 1

Vaibs_Cool
Vaibs_Cool

Reputation: 6156

 words = ['oele', 'geel', 'politie', 'foo bar'];

function markWords() {
    var html = div.html().replace(/<\/?strong>/gi, ''),
        text = html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' '),
        exp;
    $.each(words, function(i, word) {
        exp = new RegExp('\\b(' + word + ')\\b', 'gi');
        html = html.replace(exp, function(m) {
console.log('WORD MATCH:', m);
            return '<strong>' + m + '</strong>';
        });
    });
    //html = html.replace('&nbsp;', ' ').replace(/\s+/g, ' ');
console.log('HTML:', html);
console.log('----');
    div.html(html);
}

Call this function on setinterval

Fiddle

Upvotes: 4

Related Questions