How to Replace Selected Text with HTML in a Contenteditable Element

replace selected text in contenteditable div

The following will do the job in all the major browsers:

function replaceSelectedText(replacementText) {
var sel, range;
if (window.getSelection) {
sel = window.getSelection();
if (sel.rangeCount) {
range = sel.getRangeAt(0);
range.deleteContents();
range.insertNode(document.createTextNode(replacementText));
}
} else if (document.selection && document.selection.createRange) {
range = document.selection.createRange();
range.text = replacementText;
}
}

How to replace selected text with html in a contenteditable element?

See here for working jsFiddle: http://jsfiddle.net/dKaJ3/2/

function getSelectionHtml() {
var html = "";
if (typeof window.getSelection != "undefined") {
var sel = window.getSelection();
if (sel.rangeCount) {
var container = document.createElement("div");
for (var i = 0, len = sel.rangeCount; i < len; ++i) {
container.appendChild(sel.getRangeAt(i).cloneContents());
}
html = container.innerHTML;
}
} else if (typeof document.selection != "undefined") {
if (document.selection.type == "Text") {
html = document.selection.createRange().htmlText;
}
}
alert(html);
}

Code taken from Tim Down: Return HTML from a user-selected text

Replace specific word in contenteditable

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>

How to replace a custom text before caret in a contenteditable div that contains many html tags?

It seems to be sooo complicated to get/set caret position in a content-editable div element.

Anyway, I created a working snippet with the functions I found in these topics…

Get caret position in contentEditable div

Set Caret Position in 'contenteditable' div that has children

… and added a function oninput to make the replacement you wanted:

(See comments in my code for more details)

var input = document.getElementById("div_id");var replaced = "@te";  // TAKIT: Added this variablevar replacer = "@TEST"; // TAKIT: Renamed this variablevar replacer_tags = ['<span class="tagged-user" id="0">', '</span>']; // TAKIT: Added after commentinput.focus();
// TAKIT: Added functions from these solutions:// https://stackoverflow.com/questions/3972014/get-caret-position-in-contenteditable-divfunction 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;}
// https://stackoverflow.com/questions/36869503/set-caret-position-in-contenteditable-div-that-has-childrenfunction SetCaretPosition(el, pos) { // Loop through all child nodes for (var node of el.childNodes) { if (node.nodeType == 3) { // we have a text node if (node.length >= pos) { // finally add our range var range = document.createRange(), sel = window.getSelection(); range.setStart(node, pos); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); return -1; // we are done } else { pos -= node.length; } } else { pos = SetCaretPosition(node, pos); if (pos == -1) { return -1; // no need to finish the for loop } } } return pos; // needed because of recursion stuff}

// TAKIT: Added this whole functioninput.oninput = function() { var caretPos = GetCaretPosition(input); // Gets caret position if (this.innerHTML.includes(replaced)) { // Filters the calling of the function this.innerHTML = this.innerHTML.replace(replaced, replacer_tags.join(replacer) + ' '); // Replace caretPos += replacer.length - replaced.length + 1; // Offset due to strings length difference SetCaretPosition(input, caretPos); // Restores caret position } // console.clear(); console.log(this.innerHTML); // For tests}
.tagged-user {  color: #1e81d6;  font-weight: bold;}
<div contenteditable="true" id="div_id">  First person <span class="tagged-user" id="1">@Joe</span> and (add "te" to test) @ and <span class="tagged-user" id="2">@Emilie</span> want to go to the beach.</div>


Related Topics



Leave a reply



Submit