Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 886
Zerotoinfinity Avatar asked Dec 26 '14 12:12

Zerotoinfinity


2 Answers

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>
like image 92
Tobías Avatar answered Oct 04 '22 21:10

Tobías


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.

like image 45
Basj Avatar answered Oct 04 '22 20:10

Basj