I am working on a chrome extension, which replaces part of the text. It does not work as expected on Gmail though.
Scanario:
Gmail composer has following text -
i am now to this.
i can do this.
I want to replace it to -
i am new to this.
i can do this.
However, whenever I execute the code, it does not replace the text on the correct location.
It sees where my cursor was and appends the text there instead of replacing the intended text.
This snippet works on other websites, which have contenteditable editors.
My current implementation looks like the following:
const range = document.createRange();
const ele = <div tag element for 'i am now to this.' sentence>
// rangeStart and rangeEnd are the index which wraps word 'now'
range.setStart(ele.childNodes[0], rangeStart);
range.setEnd(ele.childNodes[0], rangeEnd);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
setTimeout(()=>{
document.execCommand(
"insertText",
false,
"new"
);
},0)
There are a couple of things that might go wrong about it.
One is that I noticed the div initially containing a single textNode with some text switched to a node containing multiple textNodes with exactly the same over all text and that can create a number of problems considering that you are operating only with children[0].
Another thing that didn't work for me was the document executing the isertText
How so ever something that worked for me:
var range = document.createRange();
var rangeStart = el.innerText.indexOf(s);
var rangeEnd = rangeStart + s.length;
var selection = window.getSelection();
range.setStart(el.childNodes[0], rangeStart);
range.setEnd(el.childNodes[0], rangeEnd);
selection.removeAllRanges();
selection.addRange(range);
range.deleteContents();
range.insertNode(document.createTextNode(ss))
My entire testing scenario:
(()=>{
var all = document.getElementsByTagName('div');
var n = all.length;
var find = (s)=>{
var result = [];
for (var i = 0; i < n; i++) {
var el = all[i];
var text = el && el.childNodes && el.childNodes[0] && el.childNodes[0].wholeText;
if (text && text.match(s)) {
result.push(el);
}
}
return result.length ? result : false;
}
;
var replacer = (s,ss)=>(el)=>{
try {
var range = document.createRange();
var rangeStart = el.innerText.indexOf(s);
var rangeEnd = rangeStart + s.length;
range.setStart(el.childNodes[0], rangeStart);
range.setEnd(el.childNodes[0], rangeEnd);
var selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
range.deleteContents();
range.insertNode(document.createTextNode(ss))
}catch(ex){
}
}
;
var elements = find('now');
if (elements) {
elements.map(replacer('now', 'new'))
}
}
)(window);
Good luck :)
I'm trying to find a way with javascript to highlight the text the user selects when they click some odd highlight button (as in <span style="background-color:yellow">highlighted text</span>). It only has to work with either WebKit or Firefox, but it seems to be well nigh impossible because it has to work in the following cases:
<p>this is text</p>
<p>I eat food</p>
When the user selects from "is text" through "I eat" in the browser (can't just put a span there).
and this case:
<span><span>this is text</span>middle text<span>this is text</span></span>
When the user selects from "is text" to "this is" in the browser (even though you can wrap your highlight spans around each element in the selection, I'd like to see you try to get that middle text highlighted).
This problem doesn't seem to be solved anywhere, frankly I doubt it's possible.
It would be possible if you could get the Range that you get from the selection as a string complete with html which could be parsed and then replaced, but as far as I can tell you can't get the raw html of a Range.. pity.
This answer is probably a few years too late for you, but I faced a similar problem and wanted to document it here, since it is the first hit on google.
To reiterate, the problem is that you would like to just capture the Range object from the User Selection and surround it with a styled div, like so:
function highlightSelection() {
var userSelection = window.getSelection().getRangeAt(0);
highlightRange(userSelection);
}
function highlightRange(range) {
var newNode = document.createElement("div");
newNode.setAttribute(
"style",
"background-color: yellow; display: inline;"
);
range.surroundContents(newNode);
}
But as Original Parent states, this is unsafe. It will work if the selection does not cross element boundaries, but it will throw a DOM eror if the Range created by the User Selection is an unsafe range which crosses the boundaries of HTML tags.
The solution is to produce an array of smaller Range objects, none of which individually crosses an element barrier, but which collectively cover the Range selected by the user. Each of these safe Ranges can be highlighted as above.
function getSafeRanges(dangerous) {
var a = dangerous.commonAncestorContainer;
// Starts -- Work inward from the start, selecting the largest safe range
var s = new Array(0), rs = new Array(0);
if (dangerous.startContainer != a)
for(var i = dangerous.startContainer; i != a; i = i.parentNode)
s.push(i)
;
if (0 < s.length) for(var i = 0; i < s.length; i++) {
var xs = document.createRange();
if (i) {
xs.setStartAfter(s[i-1]);
xs.setEndAfter(s[i].lastChild);
}
else {
xs.setStart(s[i], dangerous.startOffset);
xs.setEndAfter(
(s[i].nodeType == Node.TEXT_NODE)
? s[i] : s[i].lastChild
);
}
rs.push(xs);
}
// Ends -- basically the same code reversed
var e = new Array(0), re = new Array(0);
if (dangerous.endContainer != a)
for(var i = dangerous.endContainer; i != a; i = i.parentNode)
e.push(i)
;
if (0 < e.length) for(var i = 0; i < e.length; i++) {
var xe = document.createRange();
if (i) {
xe.setStartBefore(e[i].firstChild);
xe.setEndBefore(e[i-1]);
}
else {
xe.setStartBefore(
(e[i].nodeType == Node.TEXT_NODE)
? e[i] : e[i].firstChild
);
xe.setEnd(e[i], dangerous.endOffset);
}
re.unshift(xe);
}
// Middle -- the uncaptured middle
if ((0 < s.length) && (0 < e.length)) {
var xm = document.createRange();
xm.setStartAfter(s[s.length - 1]);
xm.setEndBefore(e[e.length - 1]);
}
else {
return [dangerous];
}
// Concat
rs.push(xm);
response = rs.concat(re);
// Send to Console
return response;
}
It is then possible to (appear to) highlight the User Selection, with this modified code:
function highlightSelection() {
var userSelection = window.getSelection().getRangeAt(0);
var safeRanges = getSafeRanges(userSelection);
for (var i = 0; i < safeRanges.length; i++) {
highlightRange(safeRanges[i]);
}
}
Note that you'' probably need some fancier CSS to make the many disparate elements a user could look nice together. I hope that eventually this helps some other weary soul on the internet!
Well, you can do it using DOM manipulation. This works in Firefox:
var selection = window.getSelection();
var range = selection.getRangeAt(0);
var newNode = document.createElement("span");
newNode.setAttribute("style", "background-color: pink;");
range.surroundContents(newNode);
Seems to work in the current version of Safari as well. See https://developer.mozilla.org/en/DOM/range.surroundContents and http://www.w3.org/TR/2000/REC-DOM-Level-2-Traversal-Range-20001113/ranges.html
This is my first time posting here, but looking through your answers, wouldn't something like this work? I have a sample here:
http://henriquedonati.com/projects/Extension/extension.html
function highlightSelection() {
var userSelection = window.getSelection();
for(var i = 0; i < userSelection.rangeCount; i++) {
highlightRange(userSelection.getRangeAt(i));
}
}
function highlightRange(range) {
var newNode = document.createElement("span");
newNode.setAttribute(
"style",
"background-color: yellow; display: inline;"
);
range.surroundContents(newNode);
}
Here is a complete code to highlight and dehighlight the text
<!DOCTYPE html>
<html>
<head>
<style type="text/css">
.highlight
{
background-color: yellow;
}
#test-text::-moz-selection { /* Code for Firefox */
background: yellow;
}
#test-text::selection {
background: yellow;
}
</style>
</head>
<body>
<div id="div1" style="border: 1px solid #000;">
<div id="test-text">
<h1> Hello How are you </h1>
<p >
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
</p>
</div>
</div>
<br />
</body>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script type="text/javascript">
mouseXPosition = 0;
$(document).ready(function () {
$("#test-text").mousedown(function (e1) {
mouseXPosition = e1.pageX;//register the mouse down position
});
$("#test-text").mouseup(function (e2) {
var highlighted = false;
var selection = window.getSelection();
var selectedText = selection.toString();
var startPoint = window.getSelection().getRangeAt(0).startOffset;
var endPoint = window.getSelection().getRangeAt(0).endOffset;
var anchorTag = selection.anchorNode.parentNode;
var focusTag = selection.focusNode.parentNode;
if ((e2.pageX - mouseXPosition) < 0) {
focusTag = selection.anchorNode.parentNode;
anchorTag = selection.focusNode.parentNode;
}
if (selectedText.length === (endPoint - startPoint)) {
highlighted = true;
if (anchorTag.className !== "highlight") {
highlightSelection();
} else {
var afterText = selectedText + "<span class = 'highlight'>" + anchorTag.innerHTML.substr(endPoint) + "</span>";
anchorTag.innerHTML = anchorTag.innerHTML.substr(0, startPoint);
anchorTag.insertAdjacentHTML('afterend', afterText);
}
}else{
if(anchorTag.className !== "highlight" && focusTag.className !== "highlight"){
highlightSelection();
highlighted = true;
}
}
if (anchorTag.className === "highlight" && focusTag.className === 'highlight' && !highlighted) {
highlighted = true;
var afterHtml = anchorTag.innerHTML.substr(startPoint);
var outerHtml = selectedText.substr(afterHtml.length, selectedText.length - endPoint - afterHtml.length);
var anchorInnerhtml = anchorTag.innerHTML.substr(0, startPoint);
var focusInnerHtml = focusTag.innerHTML.substr(endPoint);
var focusBeforeHtml = focusTag.innerHTML.substr(0, endPoint);
selection.deleteFromDocument();
anchorTag.innerHTML = anchorInnerhtml;
focusTag.innerHTml = focusInnerHtml;
var anchorafterHtml = afterHtml + outerHtml + focusBeforeHtml;
anchorTag.insertAdjacentHTML('afterend', anchorafterHtml);
}
if (anchorTag.className === "highlight" && !highlighted) {
highlighted = true;
var Innerhtml = anchorTag.innerHTML.substr(0, startPoint);
var afterHtml = anchorTag.innerHTML.substr(startPoint);
var outerHtml = selectedText.substr(afterHtml.length, selectedText.length);
selection.deleteFromDocument();
anchorTag.innerHTML = Innerhtml;
anchorTag.insertAdjacentHTML('afterend', afterHtml + outerHtml);
}
if (focusTag.className === 'highlight' && !highlighted) {
highlighted = true;
var beforeHtml = focusTag.innerHTML.substr(0, endPoint);
var outerHtml = selectedText.substr(0, selectedText.length - beforeHtml.length);
selection.deleteFromDocument();
focusTag.innerHTml = focusTag.innerHTML.substr(endPoint);
outerHtml += beforeHtml;
focusTag.insertAdjacentHTML('beforebegin', outerHtml );
}
if (!highlighted) {
highlightSelection();
}
$('.highlight').each(function(){
if($(this).html() == ''){
$(this).remove();
}
});
selection.removeAllRanges();
});
});
function highlightSelection() {
var selection;
//Get the selected stuff
if (window.getSelection)
selection = window.getSelection();
else if (typeof document.selection != "undefined")
selection = document.selection;
//Get a the selected content, in a range object
var range = selection.getRangeAt(0);
//If the range spans some text, and inside a tag, set its css class.
if (range && !selection.isCollapsed) {
if (selection.anchorNode.parentNode == selection.focusNode.parentNode) {
var span = document.createElement('span');
span.className = 'highlight';
span.textContent = selection.toString();
selection.deleteFromDocument();
range.insertNode(span);
// range.surroundContents(span);
}
}
}
</script>
</html>
https://jsfiddle.net/Bilalchk123/1o4j0w2v/
function load(){
window.document.designMode = "On";
//run this in a button, will highlight selected text
window.document.execCommand("hiliteColor", false, "#768");
}
<html>
<head>
</head>
<body contentEditable="true" onload="load()">
this is text
</body>
</html>
I just finished releasing a package that is a typescript port of texthighlighter (a deprecated library). Just converting it to typescript has caught a few bugs and made it easier to work on in the future. Checkout https://www.npmjs.com/package/#funktechno/texthighlighter. This has no dependencies and allows for highlighting user selection, merging highlights, removinging highlights, serializing and deserializing (applying from data) highlights.
Note you will need to use the javascript mouseup event to properly trigger it.
import { doHighlight, deserializeHighlights, serializeHighlights, removeHighlights, optionsImpl } from "#/../node_modules/#funktechno/texthighlighter/lib/index";
const domEle = document.getElementById("sandbox");
const options: optionsImpl = {};
if (this.color) options.color = this.color;
if (domEle) doHighlight(domEle, true, options);
this is how I triggered it in a vue ts project
<div
id="sandbox"
#mouseup="runHighlight($event)"
>text to highlight</div>
I was having the same problem today, highlighting the selected tags ranging over multiple tags.
The solution:
Find a way to extract the selected portion along with the HTML tags.
Wrap the extracted portion with a span element and put it back in the DOM .
Refer the code below , for further clarification.
function getRangeObject(selectionObject){
try{
if(selectionObject.getRangeAt)
return selectionObject.getRangeAt(0);
}
catch(ex){
console.log(ex);
}
}
document.onmousedown = function(e){
var text;
if (window.getSelection) {
/* get the Selection object */
userSelection = window.getSelection()
/* get the innerText (without the tags) */
text = userSelection.toString();
/* Creating Range object based on the userSelection object */
var rangeObject = getRangeObject(userSelection);
/*
This extracts the contents from the DOM literally, inclusive of the tags.
The content extracted also disappears from the DOM
*/
contents = rangeObject.extractContents();
var span = document.createElement("span");
span.className = "highlight";
span.appendChild(contents);
/* Insert your new span element in the same position from where the selected text was extracted */
rangeObject.insertNode(span);
} else if (document.selection && document.selection.type != "Control") {
text = document.selection.createRange().text;
}
};
since HTML use <mark> element as highlighted text, maybe it's easy to use this node, instead of using your own css, much more clean code:
function highlightRange(range) {
var newNode = document.createElement('mark');
range.surroundContents(newNode);
}
// original select range function
function highlight() {
var userSelection = window.getSelection();
for(var i = 0; i < userSelection.rangeCount; i++) {
highlightRange(userSelection.getRangeAt(i));
}
}
I'm using a Kendo UI Editor. I want to highlight the excess characters that are typed/pasted in the editor. This is what I've done:
$(function () {
var $editor = $('#txt-editor');
$editor.kendoEditor({
keydown: ValidateRichTextEditor
});
});
function ValidateRichTextEditor(e) {
var editor = $(e.sender.textarea),
kendoEditor = editor.data('kendoEditor'),
characters = kendoEditor.body.innerText.length,
limit = editor.data('valLengthMax');
if (characters > limit) {
var textNodes = getTextNodes(kendoEditor.body),
charCount = 0,
startNode, startOffset;
for (var i = 0, textNode; textNode = textNodes[i++];) {
var chars = charCount + textNode.length;
if (limit < chars) {
//set the text node as the starting node
//if the characters hit the limit set
startNode = textNode;
startOffset = chars - charCount;
break;
}
//add the length of the text node to the current character count
charCount += textNode.length;
}
var range = kendoEditor.createRange();
range.setStart(startNode, startOffset);
kendoEditor.selectRange(range);
kendoEditor.document.execCommand('backColor', false, '#fcc');
}
}
function getTextNodes(node) {
var textNodes = [];
//node type 3 is a text node
if (node.nodeType == 3) {
textNodes.push(node);
} else {
var children = node.childNodes;
for (var i = 0, len = children.length; i < len; i++) {
textNodes.push.apply(textNodes, getTextNodes(children[i]));
}
}
return textNodes;
}
jsfiddle
So far, the highlighting works but the cursor position is always at the position where the highlighting starts. How can I position the cursor so that it would remember the last place it was? Say for example I just keep on typing, the cursor should be at the end of the editor content. Or when I click somewhere in the middle of the content, the cursor should start where I clicked on the content.
Help on this would be greatly appreciated. Thanks!
If I am correctly interpreting your requirement, there is a much simpler solution than what you are attempting.
(function () {
var $editor = $('#txt-editor'),
limit = $editor.data('valLengthMax')
limitExceeded = false;
$editor.kendoEditor({
keyup: ValidateRichTextEditor
});
function ValidateRichTextEditor(e) {
var characters = this.body.innerText.length;
console.log('\'' + this.body.innerText + '\' : ' + this.body.innerText.length);
if (characters >= limit && !limitExceeded) {
limitExceeded = true;
this.exec('backColor', { value: '#fcc' });
}
}
})();
Update 1: This solution is a bit buggy. The backspace key causes some hiccups.
Update 2: After a lot of fiddling, you cannot trust body.innerText.length. It never returns the correct value once the background color style is executed. My reasoning is that the <span> elements that are added to the body are counted as characters and the backspace key does not remove them as would be expected.
Here is a JSBin example where you can read the console output as you type. Illogical to say the least.
I have this piece of code which is supposed to return the start and end offsets of the user selection:
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript">
function getSelRange() {
var selObj = window.getSelection();
var range = selObj.getRangeAt(0);
alert(range.startOffset+"-"+range.endOffset);
}
</script>
</head>
<body>
<button onclick="getSelRange()">Get the selected text!</button>
<p>Select some text!</p>
</body>
</html>
When I select some text from the p by dragging, it alerts the numbers correctly. However, when I select the entire text in the p by triple clicking on it, it alerts 0-1. Looks like in Firefox only, triple click doesn't return the selection range correctly.
How do I get the correct start and end points on triple click as well?
Firefox is returning the correct range. The problem is that your assumption that a range must have its start and end boundaries relative to a text node is incorrect.
What is happening is that Firefox is reporting the range as starting before the zeroth child node of the <p> element and ending after the first child of the <p> element. This is perfectly valid.
You could do something like the following to adjust such a range to make its boundaries lie inside the text node within the <p> element:
Demo: http://jsfiddle.net/DbmjH/2/
Code:
function adjustRange(range) {
range = range.cloneRange();
if (range.startContainer.nodeType != 3) {
var nodeAfterStart = range.startContainer.childNodes[range.startOffset];
if (nodeAfterStart && nodeAfterStart.nodeType == 3) {
range.setStart(nodeAfterStart, 0);
}
}
if (range.endContainer.nodeType != 3 && range.endOffset >= 1) {
var nodeBeforeEnd = range.endContainer.childNodes[range.endOffset - 1];
if (nodeBeforeEnd && nodeBeforeEnd.nodeType == 3) {
range.setEnd(nodeBeforeEnd, nodeBeforeEnd.data.length);
}
}
return range;
}
Check this fiddle ( Updated )
I have made it in a way that it will work on triple click when paragraph contains multiple lines.
<script>
function getSelRange() {
var selObj = window.getSelection();
var range = selObj.getRangeAt(0);
var r = document.getElementById('txt').innerHTML.split('<br>');
if (r[(range.endOffset-1)/2] == selObj) {
alert(0+"-"+r[(range.endOffset-1)/2].length);
} else if (range.startOffset >= range.endOffset) {
alert(range.startOffset + "-" + r[(range.endOffset-1)/2].length);
} else {
alert(range.startOffset + "-" + range.endOffset);
}
}
</script>
New Added Fiddle
<script>
function getSelRange() {
var selObj = window.getSelection();
var range = selObj.getRangeAt(0);
var r=document.getElementById('txt').innerHTML.split('<br>');
var selLines = selObj.toString().split('\n');
var Str = document.getElementById('txt').innerHTML;
Str=Str.replace(/<br>/g,"xzznlzzx");
var pr=selObj.toString().replace(/\r?\n/g,"xzznlzzx");
var rStr=Str.substring(0,Str.indexOf(pr));
var rSplit=rStr.split('xzznlzzx');
var prSplit=pr.split('xzznlzzx');
var countStart=0;
var countEnd=0;
var i=0;
for(;i<(rSplit.length-1);i++)
{
countStart=countStart+r[i].length;
}
for(j=0;j<(prSplit.length-1);i++,j++)
{
countEnd=countEnd+r[i].length;
}
countEnd=countEnd+countStart;
if(r[(range.endOffset-1)/2]==selObj)
{
alert((0+countStart)+"-"+(r[(range.endOffset-1)/2].length+countEnd));
}
else{
if(r[i].length<selObj.toString().length)
{
var indx = selObj.toString().indexOf(r[i]);
}
else{
var indx = r[i].indexOf(selObj.toString());
var vals=selObj.toString().length;
var res = r[i].substring(indx+vals,indx+vals+1);
if(res==""){indx=1}
else{indx=-1}
}
if(indx!=-1)
{
alert((range.startOffset+countStart)+"-"+(r[i].length+countEnd));
}
else{
alert((range.startOffset+countStart)+"-"+(range.endOffset+countEnd));
}
}
}
</script>
Note : for the above fiddles to work the string within <p> tag must be in a single line otherwise it will add the extra spaces between words to the range.
i am trying to alert the selection that i choose from the textarea in CKEDITOR.
when i run, just "No text is selected." comes out.
i want to see the alert "The current selection is: "+ selection.
i think i need to change here var textarea = document.getElementById('editor1');
could anyone help me with the problem??
setup:
function ()
{
var selection = "";
var textarea = document.getElementById('editor1');
if ('selectionStart' in textarea)
{
// check whether some text is selected in the textarea
if (textarea.selectionStart != textarea.selectionEnd)
{
selection = textarea.value.substring(textarea.selectionStart, textarea.selectionEnd);
}
}
else
{
// Internet Explorer before version 9
// create a range from the current selection
var textRange = document.selection.createRange();
// check whether the selection is within the textarea
var rangeParent = textRange.parentElement();
if (rangeParent === textarea)
{
selection = textRange.text;
}
}
if (selection == "")
{
alert("No text is selected.");
}
else
{
alert("The current selection is: " + selection);
}
}
To obtain the selection use the following code:
CKEDITOR.instances.youEditorInstance.getSelection().getSelectedText();
This method however returns text only (no HTML markup).
To save HTML markup, you can try something like this:
var range = CKEDITOR.instances.editor1.getSelection().getRanges()[ 0 ];
var rangeClone = range.clone();
range.collapse();
var el1 = range.getCommonAncestor( true, true );
range.splitElement( el1 );
rangeClone.collapse( true ); // to beginning
var el2 = rangeClone.getCommonAncestor( true, true );
rangeClone.splitElement( el2 );
var html = '';
var newRange = CKEDITOR.instances.editor1.getSelection().getRanges()[ 0 ];
var children = newRange.cloneContents().getChildren();
var element;
for( var i = 0 ; i < children.count() ; i++ ) {
element = children.getItem( i );
html += element.$.innerHTML ? element.getOuterHtml() : '';
}
html will store your selection HTML.