updating the range in html [duplicate] - javascript

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.

Related

Is there a way to not lose focus selection when replacing HTML with text in an contenteditable div?

I have a div tag that I make editable.
I do not want any HTML in that tag so I do not let users enter any. However, when the user does a copy / paste, it is not unlikely to include tags.
I have some jQuery code to capture the paste event and just in case I tried using my saveSelection() and restoreSelection() functions which I show below which work find in many situations but here they fail...
Fiddle: http://jsfiddle.net/9wm0oeah/2/
jQuery("#that_div").on("paste", function()
{
setTimeout(function()
{
// remove any HTML
var selection = saveSelection();
jQuery("#that_div").text(jQuery("#that_div").text());
restoreSelection(selection);
}, 0);
});
function saveSelection()
{
var sel;
if(document.selection)
{
return document.selection.createRange();
}
else
{
sel = window.getSelection();
if(sel.getRangeAt && sel.rangeCount > 0)
{
return sel.getRangeAt(0);
}
else
{
return null;
}
}
//NOTREACHED
}
function restoreSelection(range)
{
var sel;
if(document.selection)
{
range.select();
}
else
{
sel = window.getSelection();
sel.removeAllRanges();
sel.addRange((range));
}
}
Do you have any idea why it fails?
When you change the DOM nodes inside your editable div, any range exists within or partially within that div has to change to accommodate the changes. If you completely replace the content, as your code does, the nodes that the range's boundaries were relative to are destroyed and the range has to revert to a default state.
You could use a character offset-based solution instead. For example: Can't restore selection after HTML modify, even if it's the same HTML

contenteditable div issue when restore/saving selection

I have a div (#recommendTextArea) which is editable, in which that I try to modify the innerHTML of this div when a user clicks on a list (this is called .display_box), the function looks like this. Basically it appends a span to the innerHTML of the div and then it hides the friendList, upon hiding it also tries to restoreTheSelection and before appending the extra span I called saveSelection.
$(".display_box").live("click",function()
{
selRange = saveSelection();
console.log(selRange);
var username = $(this).attr('title');
var old = $("#recommendTextArea").html();
var content = old.replace(word, " "); //replacing #abc to (" ") space
var E ="<span contenteditable='false'>"+ username + "</span> ";
content = [content.slice(0, start), E, content.slice(start)].join('');
$("#recommendTextArea").html(content);
$("#friendsList").hide(function(){
restoreSelection(selRange);
});
});
I have the following function to restore and save selection:
function saveSelection() {
if (window.getSelection) {
sel = window.getSelection();
if (sel.getRangeAt && sel.rangeCount) {
return sel.getRangeAt(0);
}
} else if (document.selection && document.selection.createRange) {
return document.selection.createRange();
}
return null;
}
function restoreSelection(range) {
if (range) {
if (window.getSelection) {
sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
} else if (document.selection && range.select) {
range.select();
}
}
}
However this doesn't work as expected, the cursor is no where to be seen when I click on an item. What am I doing wrong here?
You have a few issues:
1) Timing: the "click" event is way too late to grab selection (ALWAYS debug this, it's super easy to see the editable DIV has long lost focus and selection by that time). Use "mousedown" instead.
2) You can't store selection range like this - changing the selection context (in your case the innerHTML of the commonAncestorContainer) will wipe that range (for some reason even cloned range objects get wiped for me). If you manage to get a copy (via jQuery.extend for example) it will become invalid because the text node inside is not guaranteed to remain the same. My best guess is to go with storing start/end offset and if needed the related nodes as required by the range. Restore the range properties after the HTML is modified.
3) As with 1) focus is crucial to maintain selection, so that click on the list.. make sure you prevent the default before exiting the handler so focus and you new selection will remain in the DIV.
Can't figure out the exact use case from your code but this is my test sample and you can adjust from here as needed: http://jsfiddle.net/damyanpetev/KWDf6/

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 can I highlight the text of the DOM Range object?

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.

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