How can I highlight the text of the DOM Range object? - javascript

I select some text on the html page(opened in firefox) using mouse,and using javascript functions, i create/get the rangeobject corresponding to the selected text.
userSelection =window.getSelection();
var rangeObject = getRangeObject(userSelection);
Now i want to highlight all the text which comes under the rangeobject.I am doing it like this,
var span = document.createElement("span");
rangeObject.surroundContents(span);
span.style.backgroundColor = "yellow";
Well,this works fine, only when the rangeobject(startpoint and endpoint) lies in the same textnode,then it highlights the corresponding text.Ex
<p>In this case,the text selected will be highlighted properly,
because the selected text lies under a single textnode</p>
But if the rangeobject covers more than one textnode, then it is not working properlay, It highlights only the texts which lie in the first textnode,Ex
<p><h3>In this case</h3>, only the text inside the header(h3)
will be highlighted, not any text outside the header</p>
Any idea how can i make, all the texts which comes under rangeobject,highlighted,independent of whether range lies in a single node or multiple node?
Thanks....

I would suggest using document's or the TextRange's execCommand method, which is built for just such a purpose, but is usually used in editable documents. Here's the answer I gave to a similar question:
The following should do what you want. In non-IE browsers it turns on designMode, applies a background colour and then switches designMode off again.
UPDATE
Fixed to work in IE 9.
UPDATE 12 September 2013
Here's a link detailing a method for removing highlights created by this method:
https://stackoverflow.com/a/8106283/96100
function makeEditableAndHighlight(colour) {
var range, sel = window.getSelection();
if (sel.rangeCount && sel.getRangeAt) {
range = sel.getRangeAt(0);
}
document.designMode = "on";
if (range) {
sel.removeAllRanges();
sel.addRange(range);
}
// Use HiliteColor since some browsers apply BackColor to the whole block
if (!document.execCommand("HiliteColor", false, colour)) {
document.execCommand("BackColor", false, colour);
}
document.designMode = "off";
}
function highlight(colour) {
var range;
if (window.getSelection) {
// IE9 and non-IE
try {
if (!document.execCommand("BackColor", false, colour)) {
makeEditableAndHighlight(colour);
}
} catch (ex) {
makeEditableAndHighlight(colour)
}
} else if (document.selection && document.selection.createRange) {
// IE <= 8 case
range = document.selection.createRange();
range.execCommand("BackColor", false, colour);
}
}

Rangy is a cross-browser range and selection library that solves this problem perfectly with its CSS Class Applier module. I'm using it to implement highlighting across a range of desktop browsers and on iPad and it works perfectly.
Tim Down's answer is great but Rangy spares you from having to write and maintain all that feature detection code yourself.

var userSelection = document.getSelection();
var range = userSelection.getRangeAt(0);
Instead of surroundContent method you can use the appendChild and extractContents methods this way:
let newNode = document.createElement('mark');
newNode.appendChild(range.extractContents());
range.insertNode(newNode);
function markNode() {
if(document.getSelection() && document.getSelection().toString().length){
let range = document.getSelection().getRangeAt(0);
let newNode = document.createElement('mark');
newNode.appendChild(range.extractContents());
range.insertNode(newNode);
}
else{
alert('please make selection of text to mark');
}
}
function resetContent() {
testMe.innerHTML = `Remember: Read and <strong>stay strong</strong>`;
}
<p id="testMe">Remember: Read and <strong>stay strong</strong></p>
<div><button onclick="markNode()">markNode</button></div>
<div><button onclick="resetContent()">resetContent</button></div>

Could you please elaborate the need of this functionality. If you only want to change the highlight style of the selected text you can use CSS: '::selection'
More Info:
http://www.quirksmode.org/css/selection.html
https://developer.mozilla.org/en/CSS/::selection

Can you try adding a class for the surrounding span and apply hierarchical CSS?
var span = document.createElement("span");
span.className="selection";
rangeObject.surroundContents(span);
In CSS definition,
span.selection, span.selection * {
background-color : yellow;
}
I did not try it. But just guessing that it would work.

Related

How to select and copy to clipboard text automatically with Javascript from a div? [duplicate]

In JavaScript, it's possible to programmatically select text in an input or textarea element. You can focus an input with ipt.focus(), and then select its contents with ipt.select(). You can even select a specific range with ipt.setSelectionRange(from,to).
My question is: is there any way to do this in a contenteditable element too?
I found that I can do elem.focus(), to put the caret in a contenteditable element, but subsequently running elem.select() doesn't work (and nor does setSelectionRange). I can't find anything on the web about it, but maybe I'm searching for the wrong thing...
By the way, if it makes any difference, I only need it to work in Google Chrome, as this is for a Chrome extension.
If you want to select all the content of an element (contenteditable or not) in Chrome, here's how. This will also work in Firefox, Safari 3+, Opera 9+ (possibly earlier versions too) and IE 9. You can also create selections down to the character level. The APIs you need are DOM Range (current spec is DOM Level 2, see also MDN) and Selection, which is being specified as part of a new Range spec (MDN docs).
function selectElementContents(el) {
var range = document.createRange();
range.selectNodeContents(el);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
var el = document.getElementById("foo");
selectElementContents(el);
In addition to Tim Downs answer, i made a solution that work even in oldIE:
var selectText = function() {
var range, selection;
if (document.body.createTextRange) {
range = document.body.createTextRange();
range.moveToElementText(this);
range.select();
} else if (window.getSelection) {
selection = window.getSelection();
range = document.createRange();
range.selectNodeContents(this);
selection.removeAllRanges();
selection.addRange(range);
}
};
document.getElementById('foo').ondblclick = selectText;​
Tested in IE 8+, Firefox 3+, Opera 9+, & Chrome 2+. Even I've set it up into a jQuery plugin:
jQuery.fn.selectText = function() {
var range, selection;
return this.each(function() {
if (document.body.createTextRange) {
range = document.body.createTextRange();
range.moveToElementText(this);
range.select();
} else if (window.getSelection) {
selection = window.getSelection();
range = document.createRange();
range.selectNodeContents(this);
selection.removeAllRanges();
selection.addRange(range);
}
});
};
$('#foo').on('dblclick', function() {
$(this).selectText();
});
...and who's intereseted in, here's the same for all coffee-junkies:
jQuery.fn.selectText = ->
#each ->
if document.body.createTextRange
range = document.body.createTextRange()
range.moveToElementText #
range.select()
else if window.getSelection
selection = window.getSelection()
range = document.createRange()
range.selectNodeContents #
selection.removeAllRanges()
selection.addRange range
return
Update:
If you want to select the entire page or contents of an editable region (flagged with contentEditable), you can do it much simpler by switching to designMode and using document.execCommand:
There's a good starting point at MDN and a littledocumentation.
var selectText = function () {
document.execCommand('selectAll', false, null);
};
(works well in IE6+, Opera 9+, Firefoy 3+, Chrome 2+) http://caniuse.com/#search=execCommand
The modern way of doing things is like this. More details on MDN
document.addEventListener('dblclick', (event) => {
window.getSelection().selectAllChildren(event.target)
})
<div contenteditable="true">Some text</div>
Since all of the existing answers deal with div elements, I'll explain how to do it with spans.
There is a subtle difference when selecting a text range in a span. In order to be able to pass the text start and end index, you have to use a Text node, as described here:
If the startNode is a Node of type Text, Comment, or CDATASection,
then startOffset is the number of characters from the start of
startNode. For other Node types, startOffset is the number of child
nodes between the start of the startNode.
var e = document.getElementById("id of the span element you want to select text in");
var textNode = e.childNodes[0]; //text node is the first child node of a span
var r = document.createRange();
var startIndex = 0;
var endIndex = textNode.textContent.length;
r.setStart(textNode, startIndex);
r.setEnd(textNode, endIndex);
var s = window.getSelection();
s.removeAllRanges();
s.addRange(r);
Rangy allows you to do this cross-browser with the same code. Rangy is a cross-browser implementation of the DOM methods for selections. It is well tested and makes this a lot less painful. I refuse to touch contenteditable without it.
You can find rangy here:
http://code.google.com/p/rangy/
With rangy in your project, you can always write this, even if the browser is IE 8 or earlier and has a completely different native API for selections:
var range = rangy.createRange();
range.selectNodeContents(contentEditableNode);
var sel = rangy.getSelection();
sel.removeAllRanges();
sel.addRange(range);
Where "contentEditableNode" is the DOM node that has the contenteditable attribute. You might fetch it like this:
var contentEditable = document.getElementById('my-editable-thing');
Or if jQuery is part of your project already and you find it convenient:
var contentEditable = $('.some-selector')[0];
[Updated to fix mistake]
Here is an example that is adapted from this answer that appears to work well in Chrome - Select range in contenteditable div
var elm = document.getElementById("myText"),
fc = elm.firstChild,
ec = elm.lastChild,
range = document.createRange(),
sel;
elm.focus();
range.setStart(fc,1);
range.setEnd(ec,3);
sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
HTML is:
<div id="myText" contenteditable>test</div>

Highlight text in javascript like Evernote Web Clipper

My current solution is:
Get selected html (include text and html tag), namely: selText
highlightText = <span>selText</span>
Find selText in innerHTML of the body or document (or the element which the mouse dragged in)
Replace with highlightText
But if the document is: a a a a a a and user selects the last a. My function will highlight the first or all a.
Any suggestion?
Thank you.
i think your question is duplicated, anyway i just searched the internet and found this article.
Below the final code to achieve what you ask
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-green';
range.surroundContents(span);
}
}
}
I also found this library rangy that is an helper you can use to select text but only works with jquery so i prefer the first vanilla-js solution.
var el = $("<span></span>");
el.text(rangy.getSelection().getRangeAt(0).toString());
rangy.getSelection().getRangeAt(0).deleteContents();
rangy.getSelection().getRangeAt(0).insertNode(el.get(0));
rangy.getSelection().getRangeAt(0).getSelection().setSingleRange(range);
On Range and User Selection
You have to select range using Document.createRange that return a Range object before you can use Range.surroundContents(), you could create a range this way.
var range = document.createRange();
range.setStart(startNode, startOffset);
range.setEnd(endNode, endOffset);
In practice you follow this guide to understand range and selection tecniques.
The most important point is contained in this code
var userSelection;
if (window.getSelection) {
userSelection = window.getSelection();
}
else if (document.selection) { // should come last; Opera!
userSelection = document.selection.createRange();
}
After this you can use
userSelection.surroundContents()

updating the range in html [duplicate]

I'm trying to make a JavaScript bookmarklet that will act as a highlighter, changing the background of selected text on a webpage to yellow when the bookmarklet is pressed.
I'm using the following code to get the selected text, and it works fine, returning the correct string
function getSelText() {
var SelText = '';
if (window.getSelection) {
SelText = window.getSelection();
} else if (document.getSelection) {
SelText = document.getSelection();
} else if (document.selection) {
SelText = document.selection.createRange().text;
}
return SelText;
}
However, when I created a similar function to change the CSS of the selected text using jQuery, it isn't working:
function highlightSelText() {
var SelText;
if (window.getSelection) {
SelText = window.getSelection();
} else if (document.getSelection) {
SelText = document.getSelection();
} else if (document.selection) {
SelText = document.selection.createRange().text;
}
$(SelText).css({'background-color' : 'yellow', 'font-weight' : 'bolder'});
}
Any ideas?
The easiest way to do this is to use execCommand(), which has a command to change the background colour in all modern browsers.
The following should do what you want on any selection, including ones spanning multiple elements. In non-IE browsers it turns on designMode, applies a background colour and then switches designMode off again.
UPDATE
Fixed in IE 9.
function makeEditableAndHighlight(colour) {
var range, sel = window.getSelection();
if (sel.rangeCount && sel.getRangeAt) {
range = sel.getRangeAt(0);
}
document.designMode = "on";
if (range) {
sel.removeAllRanges();
sel.addRange(range);
}
// Use HiliteColor since some browsers apply BackColor to the whole block
if (!document.execCommand("HiliteColor", false, colour)) {
document.execCommand("BackColor", false, colour);
}
document.designMode = "off";
}
function highlight(colour) {
var range, sel;
if (window.getSelection) {
// IE9 and non-IE
try {
if (!document.execCommand("BackColor", false, colour)) {
makeEditableAndHighlight(colour);
}
} catch (ex) {
makeEditableAndHighlight(colour)
}
} else if (document.selection && document.selection.createRange) {
// IE <= 8 case
range = document.selection.createRange();
range.execCommand("BackColor", false, colour);
}
}
Here is a crude example of how it could work. As Zack points out you'll need to be aware of cases where the selection spans multiple elements. This isn't intended to be used as-is, just something to help get ideas flowing. Tested in Chrome.
var selection = window.getSelection();
var text = selection.toString();
var parent = $(selection.focusNode.parentElement);
var oldHtml = parent.html();
var newHtml = oldHtml.replace(text, "<span class='highlight'>"+text+"</span>");
parent.html( newHtml );
To make the highlight stick permanently, I believe you are going to have to wrap the selection in a new DOM element (span should do), to which you can then attach style properties. I don't know if jQuery can do that for you. Keep in mind that selections can span element boundaries, so in the general case you're going to have to inject a whole bunch of new elements
Have a look at a little example i made at http://www.jsfiddle.net/hbwEE/3/
It does not take into account selections that span multiple elements..
(IE will do but will mess the html a bit ..)
In Firefox, you can use the ::-moz-selection psuedo-class.
In Webkit, you can use the ::selection pseudo-class.
I like Tim's answer, it's clean and fast. But it also shuts down the doors to doing any interactions with the highlights.
Inserting inline elements directly around the texts is a bad choice, as they broke the text flow and mess things up in complex situations,
So I suggest a dirty hack that
calculates the absolute layout of each line of selected text (no matter where they are),
then insert colored, semi-transparent inline-block elements in the end of the document body.
This chrome extension is an example of how this can be done.
It uses API from this library to get the absolute layout of each selected line.

boundary-points of a range does not meet specific requirements

I am writing a script on which the user needs to be able to select some text which is sent via ajax to the backend script for further process.
I can select plain text nodes fine or text nodes that have bold, italic or underlined text inside it.
For e.g
<p>This is <strong>some</strong> cool <em>italic</em> text, <u>really!</u></p>
So, that works, that is cool.
However, the issue is, if the text node starts with hsome bold, italic or underlined text OR even headings it outputs the following error on firefox console:
The boundary-points of a range does not meet specific requirements." code: "1 range.surroundContents($('<span...wAnno_'+newLen+'"></span>').get(0));
The error is output when the user selects something like:
<strong>Mark says</strong> Hi
OR
<em>Mark says</em> Hi
OR
<u>Mark says</u> Hi
The same error outputs even if a text is enclosed inside heading tags e.g <h2>test</h2>
My code looks like:
var select = window.getSelection();
var parents = $(select.focusNode).parents('.the-content');
if($(select.focusNode).parent().hasClass('.highlighted')) {
alert('This text is already highlighted');
} else {
for(var i = 0; i < select.rangeCount; i++) {
var range = select.getRangeAt(i);
range.surroundContents($('<span class="newHighlight" id="newHigh_'+newLen+'"></span>').get(0));
}
}
var selectedText = select.toString();
I need help with fixing this.
Help with the code will be awesome.
The problem is that the surroundContents method of Range can't work on a Range where the start and end boundaries lie within different elements, because surrounding the contents of such a Range within an element would not produce valid HTML. If changing the background colour of your Range is all you need to do, you could use the following trick with document.execCommand:
function highlight(colour) {
var range, sel;
if (window.getSelection) {
// Non-IE case
sel = window.getSelection();
if (sel.getRangeAt) {
range = sel.getRangeAt(0);
}
document.designMode = "on";
if (range) {
sel.removeAllRanges();
sel.addRange(range);
}
// Use HiliteColor since some browsers apply BackColor to the whole block
if ( !document.execCommand("HiliteColor", false, colour) ) {
document.execCommand("BackColor", false, colour);
}
document.designMode = "off";
} else if (document.selection && document.selection.createRange) {
// IE case
range = document.selection.createRange();
range.execCommand("BackColor", false, colour);
}
}
Otherwise, you'll need to walk through the text nodes within the range and surround each with a <span>, which is not trivial. I've been working on a cross-browser range and selection library that includes a module for applying a CSS class to the contents of a selection or Range at http://code.google.com/p/rangy/, although that module is a few days away from being documented and released.

How do I find out the DOM node at cursor in a browser's editable content window using Javascript?

I am looking for a solution that works cross browser i.e. IE, Firefox and Safari.
By "editable content window" I'm going to assume you mean an element with contenteditable turned on or a document with designMode turned on.
There are also two cases to consider: the case when the user has made a selection and the case where there is just a caret. The code below will work in both cases, and will give you the innermost element that completely contains the selection. If the selection is completely contained within a text node it's slightly complicated to get that text node in IE (trivial in other browsers), so I haven't provided that code here. If you need it, I can dig it out.
function getSelectionContainerElement() {
var range, sel, container;
if (document.selection && document.selection.createRange) {
// IE case
range = document.selection.createRange();
return range.parentElement();
} else if (window.getSelection) {
sel = window.getSelection();
if (sel.getRangeAt) {
if (sel.rangeCount > 0) {
range = sel.getRangeAt(0);
}
} else {
// Old WebKit selection object has no getRangeAt, so
// create a range from other selection properties
range = document.createRange();
range.setStart(sel.anchorNode, sel.anchorOffset);
range.setEnd(sel.focusNode, sel.focusOffset);
// Handle the case when the selection was selected backwards (from the end to the start in the document)
if (range.collapsed !== sel.isCollapsed) {
range.setStart(sel.focusNode, sel.focusOffset);
range.setEnd(sel.anchorNode, sel.anchorOffset);
}
}
if (range) {
container = range.commonAncestorContainer;
// Check if the container is a text node and return its parent if so
return container.nodeType === 3 ? container.parentNode : container;
}
}
}
You can also use the Rangy Library:
elementAtCursor = rangy.getSelection().anchorNode.parentNode

Categories

Resources