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 want to select the HTML of whatever the user selects in a contenteditable div. I found some code to retrieve the HTML of the selection, but it's not limited to just the div.
What I want to do is copy the selected HTML, wrap tags around it, and then replace the selection with it. So, 'test' would become 'test' for instance.
<div contenteditable="true" class="body" id="bodydiv"></div>
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;
}
}
}
HI for getting content(text) inside a selected div I have create a little code you can check it out if it works for you :
$(document).ready(function(){
$(document).bind('mouseup', function(){
var content = getSelected();
content = "<b>"+content+"</b>";
$('#selected').html(content);
});
});
function getSelected(){
var t;
if(window.getSelection){
t = window.getSelection();
var start = t.focusOffset;
var end = t.baseOffset;
t = t.anchorNode.data;
t = t.slice(start, end);
} else if (document.selection) {
t = document.selection.createRange().text;
}
return t;
}
And
<div id="selected"></div>
Hope this helps.
I have added a button to insert some text from a textarea to an editable DIV using this function found on stakoverflow.
function insertAtCursor(){
document.getElementById('myInstance1').focus() ; // DIV with cursor is 'myInstance1' (Editable DIV)
var sel, range, html;
var text = document.getElementById('AreaInsert').value ; // Textarea containing the text to add to the myInstance1 DIV
if (window.getSelection) {
sel = window.getSelection();
if (sel.getRangeAt && sel.rangeCount) {
range = sel.getRangeAt(0);
range.deleteContents();
range.insertNode( document.createTextNode(text) );
}
} else if (document.selection && document.selection.createRange) {
document.selection.createRange().text = text;
}
}
With Internet Explorer using document.selection.createRange().text it works fine for line breaks.
With Firefox and Chrome, line breaks of the textarea are not respected, all the text inserted to the editable div from the textarea is on only one line.
How to modify insertAtCursor() to make it works for line breaks with Firefox and Chrome ?
I suggest splitting the text up into separate text nodes, replacing the line breaks with <br> elements, creating a DocumentFragment containing the text and <br> nodes and calling insertNode() to insert it.
Demo: http://jsfiddle.net/timdown/zfggy/
Code:
function insertAtCursor(){
document.getElementById('myInstance1').focus() ; // DIV with cursor is 'myInstance1' (Editable DIV)
var sel, range;
var text = document.getElementById('AreaInsert').value ; // Textarea containing the text to add to the myInstance1 DIV
if (window.getSelection) {
sel = window.getSelection();
if (sel.getRangeAt && sel.rangeCount) {
range = sel.getRangeAt(0);
range.deleteContents();
var lines = text.replace("\r\n", "\n").split("\n");
var frag = document.createDocumentFragment();
for (var i = 0, len = lines.length; i < len; ++i) {
if (i > 0) {
frag.appendChild( document.createElement("br") );
}
frag.appendChild( document.createTextNode(lines[i]) );
}
range.insertNode(frag);
}
} else if (document.selection && document.selection.createRange) {
document.selection.createRange().text = text;
}
}
I think I found a more appropriate solution for your problem. For demonstration see this Fiddle. See also the css property word-wrap.
Java Script:
var button = document.getElementById('insertText');
button.onclick = function() {
var text = document.getElementById('textarea').value;
document.getElementById('insertHere').innerText = document.getElementById('insertHere').textContent = text
};
To achieve cross browser compatibility, you could also do this:
var isIE = (window.navigator.userAgent.indexOf("MSIE") > 0);
if (! isIE) {
HTMLElement.prototype.__defineGetter__("innerText",
function () { return(this.textContent); });
HTMLElement.prototype.__defineSetter__("innerText",
function (txt) { this.textContent = txt; });
}
I'm using this code to select text from the window document. This code is working fine in all browsers: it returns the selected text, but in IE8 it does not give the selected text. Instead it gives the whole HTML of the selected line. Can anybody give me solution for this?
Example:
<B><U><SPAN style="LINE-HEIGHT: 150%; FONT-FAMILY: 'Arial',
'sans-serif'; FONT-SIZE: 12pt">Summary</SPAN></U></B>
I want only Summary so all major browser return this except IE8.
<script type="text/javascript">
function getSelectionText(id) {
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;
}
}
document.getElementById(id).value = html;
//document.getElementById(id).value = html;
}
</script>
You should look into rangy - A cross-browser JavaScript range and selection library. In their own words:
It provides a simple standards-based API for performing common DOM Range and Selection tasks in all major browsers, abstracting away the wildly different implementations of this functionality between Internet Explorer up to and including version 8 and DOM-compliant browsers.
Change
html = container.innerHTML;
into
html = container.innerText;
The issue is with the following line in IE8
html = document.selection.createRange().htmlText;
Change it to this
html = document.selection.createRange().text;
Below is a nice simple function which seems to work well for me
function getSelectedText() {
var txt = '';
if (window.getSelection) {
txt = window.getSelection();
}
else if (document.getSelection) {
txt = document.getSelection();
}
else if (document.selection) {
txt = document.selection.createRange().text;
}
else return;
return txt;
}
Hope this helps
I have this function which removes selected (cursor) highlighted elements:
function deleteSelection() {
if (window.getSelection) {
// Mozilla
var selection = window.getSelection();
if (selection.rangeCount > 0) {
window.getSelection().deleteFromDocument();
window.getSelection().removeAllRanges();
}
} else if (document.selection) {
// Internet Explorer
var ranges = document.selection.createRangeCollection();
for (var i = 0; i < ranges.length; i++) {
ranges[i].text = "";
}
}
}
What I actually want to do is remove all the cursor- highlighted elements, except for the input#cursor element.
edit:
so lets say if I highlighted these elements with my cursor:
<span>a</span>
<span>b</span>
<input type='text' id = 'cursor' />
<span>c</span>
<span>d</span>
on a keyup, this function should not remove the <input> ...only the <span>
Phew, that was a little harder than expected!
Here's the code, I've tested it in IE8 and Chrome 16/FF5. The JSFiddle I used to test is available here. AFAIK, this is probably the best performance-wise you'll get.
The docs I used for Moz: Selection, Range, DocumentFragment. IE: TextRange
function deleteSelection() {
// get cursor element
var cursor = document.getElementById('cursor');
// mozilla
if(window.getSelection) {
var selection = window.getSelection();
var containsCursor = selection.containsNode(cursor, true);
if(containsCursor) {
var cursorFound = false;
for(var i=0; i < selection.rangeCount; i++) {
var range = selection.getRangeAt(i);
if(!cursorFound) {
// extracts tree from DOM and gives back a fragment
var contents = range.extractContents();
// check if tree fragment contains our cursor
cursorFound = containsChildById(contents, 'cursor');
if(cursorFound) range.insertNode(cursor); // put back in DOM
}
else {
// deletes everything in range
range.deleteContents();
}
}
}
else {
selection.deleteFromDocument();
}
// removes highlight
selection.removeAllRanges();
}
// ie
else if(document.selection) {
var ranges = document.selection.createRangeCollection();
var cursorFound = false;
for(var i=0; i < ranges.length; i++) {
if(!cursorFound) {
// hacky but it will work
cursorFound = (ranges[i].htmlText.indexOf('id=cursor') != -1);
if(cursorFound)
ranges[i].pasteHTML(cursor.outerHTML); // replaces html with parameter
else
ranges[i].text = '';
}
else {
ranges[i].text = '';
}
}
}
}
// simple BFS to find an id in a tree, not sure if you have a
// library function that does this or not
function containsChildById(source, id) {
q = [];
q.push(source);
while(q.length > 0) {
var current = q.shift();
if(current.id == id)
return true;
for(var i=0; i < current.childNodes.length; i++) {
q.push(current.childNodes[i]);
}
}
return false;
}
I would suggest removing the element you want keep, extracting the contents of the selection using Range's extractContents() method and then reinserting the element you want to keep using the range's insertNode() method.
For IE < 9, which doesn't support DOM Range, the code is trickier, so for simplicity you could use my Rangy library, which also adds convenience methods such as the containsNode() method of Range.
Live demo: http://jsfiddle.net/DFxH8/1/
Code:
function deleteSelectedExcept(node) {
var sel = rangy.getSelection();
if (!sel.isCollapsed) {
for (var i = 0, len = sel.rangeCount, range, nodeInRange; i < len; ++i) {
range = sel.getRangeAt(i);
nodeInRange = range.containsNode(node);
range.extractContents();
if (nodeInRange) {
range.insertNode(node);
}
}
}
}
deleteSelectedExcept(document.getElementById("cursor"));