contenteditable div issue when restore/saving selection - javascript

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/

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

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.

Wrap certain word with <span> using jquery

I have the following div:
<div id="query" style="width:500px; height:200px;border:1px solid black"
spellcheck="false" contenteditable="true"></div>​
where Clients can write their SQL queries. What I was trying to do is wrap words the client enters right after hitting Space with a span and give this span a certain class according to the word typed:
example
If the client types select i need to wrap this select word like this in the div:
<span class='select'> SELECT </span> <span> emp_name </span>
CSS
.select{color:blue ;text-transform:uppercase;}
It is something very similar to what jsFiddle does. How can i achieve this?
Here is what i have tried so far : jsFiddle
$(function(){
$('div').focus() ;
$('div').keyup(function(e){
//console.log(e.keyCode) ;
if(e.keyCode == 32){
var txt = $('div').text() ;
var x = 'SELECT' ;
$('div:contains("'+x+'")').wrap("<span style='color:blue ;
text-transform:uppercase;'>") ;
if(txt == 'SELECT'){
console.log('found') ; // why This Doesn't do any thing ?
}
}
});
});
I did a proof of concept with some modifications from what you originally had. See below,
DEMO: http://jsfiddle.net/cgy69/
$(function() {
$('div').focus();
var x = ['SELECT', 'WHERE', 'FROM'];
$('div').keyup(function(e) {
//console.log(e.keyCode) ;
if (e.keyCode == 32) {
//using .text() remove prev span inserts
var text = $.trim($(this).text()).split(' ');
$.each(text, function(i, v) {
$.each(x, function(j, xv) {
if (v.toUpperCase() === xv) {
text[i] = '<span style="color: blue; text-transform: uppercase;">' + v + '</span>';
}
});
});
$(this).html(text.join(' ') + ' ');
setEndOfContenteditable(this);
}
});
function setEndOfContenteditable(contentEditableElement) {
var range, selection;
if (document.createRange) //Firefox, Chrome, Opera, Safari, IE 9+
{
range = document.createRange(); //Create a range (a range is a like the selection but invisible)
range.selectNodeContents(contentEditableElement); //Select the entire contents of the element with the range
range.collapse(false); //collapse the range to the end point. false means collapse to end rather than the start
selection = window.getSelection(); //get the selection object (allows you to change selection)
selection.removeAllRanges(); //remove any selections already made
selection.addRange(range); //make the range you have just created the visible selection
}
else if (document.selection) //IE 8 and lower
{
range = document.body.createTextRange(); //Create a range (a range is a like the selection but invisible)
range.moveToElementText(contentEditableElement); //Select the entire contents of the element with the range
range.collapse(false); //collapse the range to the end point. false means collapse to end rather than the start
range.select(); //Select the range (make it the visible selection
}
}
});
You going to extend this further to handle
Backspace
HTML contents from previous inserts
Cursor position Partially done, editing in the middle would still mess up the caret.
and more..
Starting with a contenteditable element we can replace the markup as we need by operating directly on its innerHtml:
$('#query-container').on('keyup', function(e){
var $this = $(this);
//(?!\<\/b\>) negative lookahead is used so that anything already wrapped
//into a markup tag would not get wrapped again
$this.html($this.html().replace(/(SELECT|UPDATE|DELETE)(?!\<\/b\>)/gi, '<b>$1</b>'));
setEndOfContenteditable(this);
});
IMO this is a more readable option. Add the rangeselect method from the previous answer and we have a working fiddle

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