var input = document.getElementById("div_id");
var username = "TEST";
input.focus();
var selection = window.getSelection();
var range = selection.getRangeAt(0);
var span = document.createElement('span');
span.setAttribute('class', 'tagged-user');
span.setAttribute('id', 55);
span.innerHTML = username;
//var initialText = input.textContent;
//var before_caret = initialText.slice(0,getCaretPosition(input));
// var after_caret = initialText.slice(getCaretPosition(input), initialText.length);
// // var before_caret = input.textContent;
// console.log("before_caret " + before_caret);
// *******************************
//this is the regex that match last #something before caret and it work good.
//var droped_at = before_caret.replace(/#\w+(?!.*#\w+)/g, '');
// *******************************
// input.textContent = droped_at + after_caret;
// console.log("before_caret " + before_caret);
range.deleteContents();
range.insertNode(span);
range.collapse(false);
range.insertNode(document.createTextNode("\u00a0"));
// cursor at the last
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
.tagged-user {
color: #1e81d6;
font-weight: bold;
}
<div contenteditable="true" id="div_id">
First person <span class="tagged-user" id="1">#Joe </span> and #te and <span class="tagged-user" id="2">#Emilie</span> want to go to the beach.
</div>
An example of a text:
hello man, #te #emilie how are you?( #emilie is a span tag)
In this example if the cursor is after #te, I want to replace #te by TEST.
Hello,
I am trying to make a facebook like mention system. I'm able to trigger a message that shows a list of all my contacts as soon as I type "#".
Everything is working fine, the only problem is when I type "#te" for finding "TEST" in the list, it should be able to replace "#te" with "TEST". It inputs #teTEST, any clue how to do it ? I'm using a div with contenteditable.
Thanks a lot.
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 variable
var replacer = "#TEST"; // TAKIT: Renamed this variable
var replacer_tags = ['<span class="tagged-user" id="0">', '</span>']; // TAKIT: Added after comment
input.focus();
// TAKIT: Added functions from these solutions:
// https://stackoverflow.com/questions/3972014/get-caret-position-in-contenteditable-div
function 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-children
function 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 function
input.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>
Hope it helps.
I have the following code taken from Pranav C Balan's answer to my previous question:
var div = document.getElementById('div');
div.addEventListener('input', function() {
var pos = getCaretCharacterOffsetWithin(this);
// get all red subtring and wrap it with span
this.innerHTML = this.innerText.replace(/red/g, '<span style="color:red">$&</span>')
setCaretPosition(this, pos);
})
// following code is copied from following question
// https://stackoverflow.com/questions/26139475/restore-cursor-position-after-changing-contenteditable
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 setCaretPosition(element, offset) {
var range = document.createRange();
var sel = window.getSelection();
//select appropriate node
var currentNode = null;
var previousNode = null;
for (var i = 0; i < element.childNodes.length; i++) {
//save previous node
previousNode = currentNode;
//get current node
currentNode = element.childNodes[i];
//if we get span or something else then we should get child node
while (currentNode.childNodes.length > 0) {
currentNode = currentNode.childNodes[0];
}
//calc offset in current node
if (previousNode != null) {
offset -= previousNode.length;
}
//check whether current node has enough length
if (offset <= currentNode.length) {
break;
}
}
//move caret to specified offset
if (currentNode != null) {
range.setStart(currentNode, offset);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
}
<span contenteditable="true" id="div" style="width:100%;display:block">sss</span>
It has a editable <div> where the user can input and it automatically colors the word red as red just like some code editors color key words like HTML tags, strings, functions, etc.Type "red" and you will understand what I mean.
The issue I'm having is, when I type "<", it deletes all the characters in front of it unless it finds a ">" where it will stop. Another error happens if you type "" (or any other number instead of 1 really).
Any ideia on how to prevent this behavior?
You're running into this problem because you're expecting the user to be able to input HTML-like entities such as <xyz... or { but don't want to parse that input as HTML, but at the same time, you're yourself putting html elements in the same div and you want that to be parsed as HTML. So there are two ways you can go about this:
Keep the input and presentation separate. So user can input anything, which you'll sanitize and display in an output box.
Or... change the addEventListener function:
div.addEventListener('input', function() {
var pos = getCaretCharacterOffsetWithin(this);
var userString = sanitizeHTML(this.innerText);
// get all red subtring and wrap it with span
this.innerHTML = userString.replace(/red/g, '<span style="color:red">$&</span>')
setCaretPosition(this, pos);
})
This would work in most scenarios, but it'd break (badly) if you're expecting user to input HTML too, for example <span class="red" style="color: red">red</span> would become horribly mutilated. Other than that, you're good to go. Get sanitizeHTML from here: https://github.com/punkave/sanitize-html
I have this contentedittable div
<div contenteditable="true" id="text">minubyv<img src="images/smiley/Emoji Smiley-01.png" class="emojiText" />iubyvt</div>
Here is an image description of the code output
so I want to get the caret position of the div and lets assume that the cursor is after the last character. And this is my code for getting the caret position
function getCaretPosition(editableDiv) {
var caretPos = 0,
sel, range;
if (window.getSelection) {
sel = window.getSelection();
if (sel.rangeCount) {
range = sel.getRangeAt(0);
if (range.commonAncestorContainer.parentNode == editableDiv) {
caretPos = range.endOffset;
}
}
} else if (document.selection && document.selection.createRange) {
range = document.selection.createRange();
if (range.parentElement() == editableDiv) {
var tempEl = document.createElement("span");
editableDiv.insertBefore(tempEl, editableDiv.firstChild);
var tempRange = range.duplicate();
tempRange.moveToElementText(tempEl);
tempRange.setEndPoint("EndToEnd", range);
caretPos = tempRange.text.length;
}
}
return caretPos;
}
var update = function() {
console.log(getCaretPosition(this));
};
$('#text').on("mousedown mouseup keydown keyup", update);
But the problem is that it returns 6 instead of 14. The caret position goes back to 0 after the image. Please is there a way I can get the caret position to be 14 in this case.
EDIT
I want to also insert some element starting from the caret position. so this is my function to do that
selectStart = 0;
var update = function() {
selectStart = getCaretPosition(this);
};
function insertEmoji(svg){
input = $('div#text').html();
beforeCursor = input.substring(0, selectStart);
afterCursor = input.substring(selectStart, input.length);
emoji = '<img src="images/smiley/'+svg+'.png" class="emojiText" />';
$('div#text').html(beforeCursor+emoji+afterCursor);
}
See Tim Down's answer on Get a range's start and end offset's relative to its parent container.
Try to use the function he has to get the selection index with nested elements like this:
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;
}
var update = function() {
console.log(getCaretCharacterOffsetWithin(this));
};
$('#text').on("mousedown mouseup keydown keyup", update);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div contenteditable="true" id="text">minubyv<img src="https://themeforest.net/images/smileys/happy.png" class="emojiText" />iubyvt</div>
I wrote my own function, based on Tim Down's, that works like you want it. I changed the treeWalker to filter NodeFilter.ELEMENT_NODE insted of NodeFilter.SHOW_TEXT, and now <img/> elements also get processed inside our loop. I start by storing the range.startOffset and then recurse through all the selection tree nodes. If it finds an img node, then it adds just 1 to the position; if the current node element is different than our range.startContainer, then it adds that node's length. The position is altered by a different variable lastNodeLength that is adds to the charCount at each loop. Finally, it adds whatever is left in the lastNodeLength to the charCount when it exists the loop and we have the correct final caret position, including image elements.
Final working code (it returns 14 at the end, exactly as it should and you want)
function getCharacterOffsetWithin_final(range, node) {
var treeWalker = document.createTreeWalker(
node,
NodeFilter.ELEMENT_NODE,
function(node) {
var nodeRange = document.createRange();
nodeRange.selectNodeContents(node);
return nodeRange.compareBoundaryPoints(Range.END_TO_END, range) < 1 ?
NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
},
false
);
var charCount = 0, lastNodeLength = 0;
if (range.startContainer.nodeType == 3) {
charCount += range.startOffset;
}
while (treeWalker.nextNode()) {
charCount += lastNodeLength;
lastNodeLength = 0;
if(range.startContainer != treeWalker.currentNode) {
if(treeWalker.currentNode instanceof Text) {
lastNodeLength += treeWalker.currentNode.length;
} else if(treeWalker.currentNode instanceof HTMLBRElement ||
treeWalker.currentNode instanceof HTMLImageElement /* ||
treeWalker.currentNode instanceof HTMLDivElement*/)
{
lastNodeLength++;
}
}
}
return charCount + lastNodeLength;
}
var update = function() {
var el = document.getElementById("text");
var range = window.getSelection().getRangeAt(0);
console.log("Caret pos: " + getCharacterOffsetWithin_final(range, el))
};
$('#text').on("mouseup keyup", update);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div contenteditable="true" id="text">minubyv<img contenteditable="true" src="https://themeforest.net/images/smileys/happy.png" class="emojiText" />iubyvt</div>
I'm making a demo to find the closest parent html what contains specific cursor position.
HTML:
[contenteditable=true] {
border: 3px solid #ccc
}
<span contenteditable="true"><span id="sp1">[child span text 1]</span>[span text without child]<span id="sp2">[child span text 2]</span></span>
JavaScript code to get the caret index:
$.fn.extend({
getCaretIndex: function () {
var $self = $(this)[0], index = 0, sel, range;
if (window.getSelection) {
sel = window.getSelection();
if (sel.rangeCount) {
range = sel.getRangeAt(0);
if (range.commonAncestorContainer.parentNode === $self) {
index = range.endOffset
}
}
} else if (document.selection && document.selection.createRange) {
range = document.selection.createRange();
if (range.parentElement() === $self) {
var tempEl = document.createElement('span');
$self.insertBefore(tempEl, $self.firstChild);
var tempRange = range.duplicate();
tempRange.moveToElementText(tempEl);
tempRange.setEndPoint('EndToEnd', range);
index = tempRange.text.length
}
}
return index
}
});
// In use:
var index = $('span[contenteditable="true"]').getCaretIndex();
Requirement: when I click on the span[contenteditable="true"], I will find the caret index and get the html $('span[contenteditable="true"]').html() first.
$('span[contenteditable="true"]').click(function () {
var $self = $(this),
html = $self.html(),
index = $self.getCaretIndex();
});
Then, if the index is inside <span id="sp1"> tag, show that tag within .text() via html.
var tag = $('span#sp1'), text = tag.text();
Where I'm stuck: The index would be calculated via $self.text(). So, if I get the html to check where it is, that's wrong.
var html = $self.html();
var text = $self.text();
// html.length !== text.length
My question: how to get the html inside <span contenteditable> via caret index in this case?
More extension (if need):
String.prototype.contains = function (o) {
'use strict';
var $self = this;
return $.type(o) === 'string' ? $self.indexOf(o) > -1 : ($self.match(o) || []).length > 0
};
// In use:
var contained = 'My string'.contains('str'); // true
var _contained = 'My string'.contains(/\s/); // true
I hope it's clearly!
I have a html page with text content. On selecting any text and pressing the highlight button, I can change the style of the selected text to highlight the same. To implement this feature, i have written the following method.
sel = window.getSelection();
var range = sel.getRangeAt(0);
var span = document.createElement('span');
span.className = "highlight" + color;
range.surroundContents(span);
This is working fine if you choose a text with no html tag, but when the text has any html tag in between, it is giving error
Failed to execute 'surroundContents' on 'Range': The Range has partially selected a non-Text node.
How to solve this problem. Is it possible to highlight the same separately for each part(divided by html tags)?
See Range.extractContents:
document.getElementById('execute').addEventListener('click', function() {
var range = window.getSelection().getRangeAt(0),
span = document.createElement('span');
span.className = 'highlight';
span.appendChild(range.extractContents());
range.insertNode(span);
});
.highlight { background-color: yellow; }
<div id="test">
Select any part of <b>this text and</b> then click 'Run'.
</div>
<button id="execute">Run</button>
Rather than reinvent the wheel, I'd use Rangy's highlighting capabilities.
I've forked the fiddle that RGraham created and created a new fiddle that shows how it works. This is how it is done:
var applier = rangy.createClassApplier("highlight");
var highlighter = rangy.createHighlighter();
highlighter.addClassApplier(applier);
document.getElementById('execute').addEventListener('click', function() {
highlighter.removeAllHighlights();
highlighter.highlightSelection("highlight");
});
What this does is create a highlighter that will set the highlight class on elements that are wholly inside the selection, and create spans with the highlight class as needed for elements that straddle the selection. When the button with the id execute is clicked, the old highlights are removed and the new highlights applied.
The highlighter functionality is part of release of Rangy that are considered to be "alpha". However, I've been consistently using alpha releases of Rangy for a few years now but it has been extremely rare that I found a problem with my application that I could trace back to Rangy. And the few times I found a problem with Rangy, Tim Down (its author) was quite responsive.
My solution highlighting all selected nodes.
function highlight() {
const sel = window.getSelection();
const range = sel.getRangeAt(0);
const {
commonAncestorContainer,
startContainer,
endContainer,
startOffset,
endOffset,
} = range;
const nodes = [];
console.group("range");
console.log("range", range);
console.log("commonAncestorContainer", commonAncestorContainer);
console.log("startContainer", startContainer);
console.log("endContainer", endContainer);
console.log("startOffset", startOffset);
console.log("endOffset", endOffset);
console.log("startContainer.parentNode", startContainer.parentNode);
console.groupEnd();
if (startContainer === endContainer) {
const span = document.createElement("span");
span.className = "highlight";
range.surroundContents(span);
return;
}
// get all posibles selected nodes
function getNodes(childList) {
console.group("***** getNode: ", childList);
childList.forEach((node) => {
console.log("node:", node, "nodoType", node.nodeType);
const nodeSel = sel.containsNode(node, true);
console.log("nodeSel", nodeSel);
// if is not selected
if (!nodeSel) return;
const tempStr = node.nodeValue;
console.log("nodeValue:", tempStr);
if (node.nodeType === 3 && tempStr.replace(/^\s+|\s+$/gm, "") !== "") {
console.log("nodo agregado");
nodes.push(node);
}
if (node.nodeType === 1) {
if (node.childNodes) getNodes(node.childNodes);
}
});
console.groupEnd();
}
getNodes(commonAncestorContainer.childNodes);
console.log(nodes);
nodes.forEach((node, index, listObj) => {
const { nodeValue } = node;
let text, prevText, nextText;
if (index === 0) {
prevText = nodeValue.substring(0, startOffset);
text = nodeValue.substring(startOffset);
} else if (index === listObj.length - 1) {
text = nodeValue.substring(0, endOffset);
nextText = nodeValue.substring(endOffset);
} else {
text = nodeValue;
}
const span = document.createElement("span");
span.className = "highlight";
span.append(document.createTextNode(text));
const { parentNode } = node;
parentNode.replaceChild(span, node);
if (prevText) {
const prevDOM = document.createTextNode(prevText);
parentNode.insertBefore(prevDOM, span);
}
if (nextText) {
const nextDOM = document.createTextNode(nextText);
parentNode.insertBefore(nextDOM, span.nextSibling);
}
});
sel.removeRange(range);
}
Example https://codesandbox.io/s/api-selection-multiple-with-nodes-gx2is?file=/index.html
try this:
newNode.appendChild(range.extractContents())
according to MDN:
Partially selected nodes are cloned to include the parent tags
necessary to make the document fragment valid.
Whereas Range.surroundContents:
An exception will be thrown, however, if the Range splits a non-Text
node with only one of its boundary points. That is, unlike the
alternative above, if there are partially selected nodes, they will
not be cloned and instead the operation will fail.
Didn't test, but...
This solution is bit tricky, but I find it would be sufficient
When you will see closely in selection object that we get through calling
window.getSelection().getRangeAt(0)
You will se that there are 4 properties: startContainer, startOffset, endContainer, endOffset.
So now you need to start with startContainer with startOffset and start putting your necessary span nodes from there.
If now it endContainer is different node then you need to start traversing nodes from startContainer to endContainer
For traversing you need to check for child nodes and sibling nodes which you can get from DOM objects. So first go through startContainer, go through all its child and check if child node is inline element then apply span tag around it, and then you need to write few coding for various corner cases.
The solution is really tricky.
I somehow find a workaround. See my fiddle
function highlight() {
var range = window.getSelection().getRangeAt(0),
parent = range.commonAncestorContainer,
start = range.startContainer,
end = range.endContainer;
var startDOM = (start.parentElement == parent) ? start.nextSibling : start.parentElement;
var currentDOM = startDOM.nextElementSibling;
var endDOM = (end.parentElement == parent) ? end : end.parentElement;
//Process Start Element
highlightText(startDOM, 'START', range.startOffset);
while (currentDOM != endDOM && currentDOM != null) {
highlightText(currentDOM);
currentDOM = currentDOM.nextElementSibling;
}
//Process End Element
highlightText(endDOM, 'END', range.endOffset);
}
function highlightText(elem, offsetType, idx) {
if (elem.nodeType == 3) {
var span = document.createElement('span');
span.setAttribute('class', 'highlight');
var origText = elem.textContent, text, prevText, nextText;
if (offsetType == 'START') {
text = origText.substring(idx);
prevText = origText.substring(0, idx);
} else if (offsetType == 'END') {
text = origText.substring(0, idx);
nextText = origText.substring(idx);
} else {
text = origText;
}
span.textContent = text;
var parent = elem.parentElement;
parent.replaceChild(span, elem);
if (prevText) {
var prevDOM = document.createTextNode(prevText);
parent.insertBefore(prevDOM, span);
}
if (nextText) {
var nextDOM = document.createTextNode(nextText);
parent.appendChild(nextDOM);
}
return;
}
var childCount = elem.childNodes.length;
for (var i = 0; i < childCount; i++) {
if (offsetType == 'START' && i == 0)
highlightText(elem.childNodes[i], 'START', idx);
else if (offsetType == 'END' && i == childCount - 1)
highlightText(elem.childNodes[i], 'END', idx);
else
highlightText(elem.childNodes[i]);
}
}
if (window.getSelection) {
var sel = window.getSelection();
if (!sel) {
return;
}
var range = sel.getRangeAt(0);
var start = range.startContainer;
var end = range.endContainer;
var commonAncestor = range.commonAncestorContainer;
var nodes = [];
var node;
for (node = start.parentNode; node; node = node.parentNode){
var tempStr=node.nodeValue;
if(node.nodeValue!=null && tempStr.replace(/^\s+|\s+$/gm,'')!='')
nodes.push(node);
if (node == commonAncestor)
break;
}
nodes.reverse();
for (node = start; node; node = getNextNode(node)){
var tempStr=node.nodeValue;
if(node.nodeValue!=null && tempStr.replace(/^\s+|\s+$/gm,'')!='')
nodes.push(node);
if (node == end)
break;
}
for(var i=0 ; i<nodes.length ; i++){
var sp1 = document.createElement("span");
sp1.setAttribute("class", "highlight"+color );
var sp1_content = document.createTextNode(nodes[i].nodeValue);
sp1.appendChild(sp1_content);
var parentNode = nodes[i].parentNode;
parentNode.replaceChild(sp1, nodes[i]);
}
}