rangy fix boundaries - javascript

When selecting text there is some variation on exactly where the selection starts and ends as in sometimes it starts at the end of the previous element and sometimes at the start of the textnode. I'm trying to normalize this so it always starts at the beginning of the element containing the text and ends at the end of the element containing the text and make it consistent across browsers.
e.g. <b>mouse></b><i>cat</i>
When selecting "cat", chrome always seems to do the right thing and return a selection with startContainer cat and startOffset 0. Firefox and occasionally IE8 will often start at the end of the previous element (mouse) with startOffset 5
My crude attempt to fix this has not been successful:
var sr=rangy.getSelection().getRangeAt(0);
var sc=sr.startContainer;
if(sc.nodeType!=3||sr.startOffset==sc.length)
{
sr.setStartAfter(sc); //move start to next node in range
}
rangy.getSelection().setSingleRange(sr);
console.log(sr.inspect());
What am I missing?

Ok I think I've cracked it but I am very open to comments or suggestions on how to improve it. Somehow it lacks elegance and I feel there should be a better way. The problem only seems to need fixing in firefox. Chrome and IE8 never seem to select outside the element the text is contained in. Anyway this is what seems to work for me. (so far)
var sr=rangy.getSelection().getRangeAt(0);
var sc=sr.startContainer,ec=sr.endContainer;
if(sc.nodeType!=3||sr.startOffset==sc.length)
{
sc=(sc.nextSibling)?sc.nextSibling:sc.parentNode.nextSibling;
if(sc.nodeType!=3)sc=sc.firstChild;
sr.setStart(sc,0);
}
if(ec.nodeType!=3||sr.endOffset==0)
{
ec=(ec.previousSibling)?ec.previousSibling:ec.parentNode.previousSibling;
if(ec.nodeType!=3)ec=ec.lastChild;
sr.setEnd(ec,ec.length);
}
rangy.getSelection().setSingleRange(sr);
console.log(sr.inspect());

Related

Place tags around certain text within contenteditable without moving cursor

I am working on a simple (I thought) word processor. It uses contenteditable. I have a list of words that I want to always appear highlighted.
<article contenteditable="true" class="content">
<p>Once upon a time, there were a couple of paragraphs. Some things were <b>bold</b>, and other things were <i>italic.</i></p>
<p>Then down here there was the word highlight. It should have a different color background.</p>
</article>
So basically what I need is a way to wrap a word in <span> tags. This has proven more difficult than I expected.
Here was what I tried first:
var text = document.querySelector('article.content').innerHTML
start = text.indexOf("highlight"),
end = start + "highlight".length;
text = text.splice(end, 0, "</span>");
text = text.splice(start, 0, "<span>");
document.querySelector('article.content').innerHTML = text;
It uses the splice method found here.
And it does exactly what I need it to do, with one big issue: the cursor gets moved. Because all the text is replaced, the cursor loses its place, which isn't a good thing for a text editor.
I've also tried a couple times using document.createRange, but the issue is that while given the start and end points of a range only includes visible characters, text.indexOf("highlight") gives the index including the tags and such.
A few ideas which I'm not sure how to execute:
Figure out where the cursor begins and place it there again after using the code above
Find the difference in indexes between createRange and indexOf
Maybe there's already a library with this kind of functionality that I just can't find
Thank you for your help!
Firstly, I would recommend against doing this by manipulating innerHTML. It's inefficient and error-prone (think of the case where the content contains an element with a class of "highlight", for example). Here's an example of doing this using DOM methods to manipulate the text nodes directly:
https://stackoverflow.com/a/10618517/96100
Maintaining the caret position can be achieved a number of ways. You could use a character offset-based approach, which has some disadvantages due to not considering line breaks implied by <br> and block elements but is relatively simple. Alternatively, you could use the selection save and restore module of my Rangy library, which may be overkill for your needs, but the same approach could be used.
Here is an example using the first approach:
http://jsbin.com/suwogaha/1

ShadowRoot's getSelection().getRangeAt(0) returns incorrect Range object in Google Chrome 35

We're working on an application for our customer that uses dart lang with polymer components. One of our custom components namely datagrid uses <div contenteditable></div> for entering values to the datagrid cells. I also want to provide custom formatting capabilities so I had to override keypress event. The problem araises when I want to create new HTML nodes at the caret position within the content-editable div element in Chrome 35 (possibly all webkit browsers that support Shadow DOM natively, not through polyfills).
When I use window.getSelection().getRangeAt(0) to get current caret position, new nodes are being added to the beginning of the body element instead of the div. In Firefox and IE 11 it works great though (using polyfill).
When I tried this.shadowRoot.getSelection().getRangeAt(0) as suggested here it didn't work as well because it returns incorrect Range object (a bug or a feature?). However when I logged the Selection object, it seems like all offsets are correct so it means that it can be worked around somehow but the guy that asked similar question on SO hasn't posted how he did it.
So I don't know, how to create a Range object from given offset when content-editable div contains multiple HTML nodes as browsers like to create multiple nodes when you put a line break into the middle of the word and delete it immediately (as a side question, is it normal anyways? doesn't it cause memory leaks?). It then looks like this in a Chrome console (#cell-input here has 4 nodes although it just contains continuous string "Marek"):
<div contenteditable="true" id="cell-input">
"M"
"ar"
"ek"
<br>
</div>
I tried to use something like (it's just a pseudocode):
offset = shadowRoot.getSelection().extentOffset;
element = document.querySelector('cell-input');
range = document.createRange();
range.setStart(element.children.first, offset );
range.setEnd(element.children.first, offset);
range.collapse(false);
... but it didn't work properly. Any ideas?
Seems like it is a bug reported by me here and confirmed by the chromium team at the end of the issue thread.
I've created a plunk which shows how we can save shadowRoot selection range and use it when needed.
Problem is this.shadowRoot.getSelection().getRangeAt(0) returns incorrect range, you've to create range from this.shadowRoot.getSelection() by getSelection().setStart, getSelection().setEnd. For more understanding see the plunk
shadowRoot.getSelection.getRangeAt(0) instead use
var selRange=document.createRange();
var shadowRootSelection = this.shadowRoot.getSelection();
selRange.setStart(shadowRootSelection.anchorNode, shadowRootSelection.anchorOffset);
selRange.setEnd(shadowRootSelection.focusNode, shadowRootSelection.focusOffset);

CKEditor end/start of tag detect

In my Previous post:
Finding Touched elments CKEDITOR,
I was looking for the touched elements. The anwser at that post worked well.
But now i am having another problem. When the caret is set to the Front or the end of a tag/block.
I only can see that the parent is span. But it can be that the user tried to put the caret just outside the tag. example:
this ^<span>^test</span> text
The caret can be set to both "^" without seeing any difference.
(Single selection (startContainer == endContainer))
The anwser on the previous post does detect the span if it is just outside the tag. But just inside the tag it won't detect which gives a new scenario.. i Need a way to avoid the new scenario and let it handle like a touched block.
I would like to detect if the caret is inside the span and do touch the start.
The same for the end only then it would touch the end.
The function range.checkStartOfBlock returns false, this isn't a solution.
I found the fix for Mozilla, now i also want a fix for IE7 and up.
Mozilla fix:
A check for start:
range.startOffset === 0 //Means StartOfBlock
A Check for end:
range.endOffset === range.startContainer.getText().length //Means EndOfBlock
==== Edit ====
The rangy range library from rangy.googlecode.com provided an acceptable solution.
(the offer a somehow compatible startOffset and some other functionalities.)
I needed about 3 checks for Internet Explorer and only 1 for Mozilla.
But it looks like it's fool proof.
Still all ideas are very welcome.

getting selected text & text indexes in a <pre> tag, where the offset is the start of the pre

So i have a pre tag like so:
<pre> Some content, more content. <span>Coloured content</span>. Some more content</pre>
What i want to do is setup an event using javascript or jquery that binds a mouseup event. When the user selects text, i want to get the indexes offset from the start of the pre, so it ignores the span tags per say. So if someone selects the text after the span tag, it knows to offset from the pre opening.
Is there a way I can do this? It looks like window.getSelection starts it off after the span tag.
Given this HTML
<pre>0<span>1</span>23<span>4<span>56<span><span>7</span></span>8</span></span></pre>
you want to get the first selected digit as output/offset, right?
The basic idea is to navigate to the left in the DOM tree until there is no more node with the same parent. Then climb up to finally reach the pre tag. Whilst navigating through the tree towards the upper left, all characters of the visited elements are counted and added to the final result.
$('pre').on('mouseup', function(){
var selection = window.getSelection();
// Get the offset within the container that holds all of the selection
var offset = selection.anchorOffset;
// A reference to the currently examined node
var currentNode = selection.anchorNode;
// Wander around until we hit the pre
while(currentNode!==this){
if(currentNode.previousSibling){
// If there is a node left of us (with the same parent) navigate to the left and sum up the text length in the left node.
// There is no need to check the children (possibly existent) since these would be included in text's return value
offset = offset + $(currentNode.previousSibling).text().length;
// Navigate to the left node of the current
currentNode = currentNode.previousSibling;
} else {
// There is no left node so climb up towards the pre tag
currentNode = currentNode.parentNode;
}
}
// Print the final result
console.log(offset);
});
The script should output the required number. So if you are selecting 78 you'd get 7 as output.
I did only test this code in Firefox. Other browsers should work as well if they implement HTML Editing API. IE does not support it until version 9. The same applies for getSelection (see MSDN).
This kind of thing gets very complicated, especially when you need to worry about cross-browser implementations (*cough* IE *cough*). I therefore strongly recommend Rangy, a "cross-browser JavaScript range and selection library." I've used it a bit myself and found it to work perfectly.
The library's author, Tim Down, has answered a lot of questions on SO about range and selection issues, and you'll see how complicated they get :)

Selection object behaving strange in Chrome contentEditable element

I'm working on a realtime syntax highlighter in javascript using contenteditable. When parsing content I extract the text of the div and use regex patterns to style it properly. Then I set the innerHtml of the div to the parsed content. However, this makes the cursor disappear from the screen.
I have created this function to reset the cursor, and it works fine in Firefox. But in Chrome the cursor is moving in a wierd way that is semi-predictable. It's usually set at the first space-char in the document instead of the place it was right before the parsing.
The caret char that is stored in the variable cc is at the place it should be though.
/**
* Put cursor back to its original position after every parsing, and
* insert whitespace to match indentation level of the line above this one.
*/
findString : function()
{
cc = '\u2009'; // carret char
if ( self.find(cc) )
{
var selection = window.getSelection();
var range = selection.getRangeAt(0);
if ( this.do_indent && this.indent_level.length > 0 )
{
var newTextNode = document.createTextNode(this.indent_level);
range.insertNode(newTextNode);
range.setStartAfter(newTextNode);
this.do_indent = false;
}
selection.removeAllRanges();
selection.addRange(range);
}
}
Some facts about calling these functions:
When I uncomment the code that switches the innerHtml content the cursor usually moves to the end of the document instead.
If I uncomment the findString() caller the parsing is done but the cursor disappears until I put focus back in the div.
If I uncomment both lines the div is behaving as one would expect except, ofcourse, for the parsing.
What is causing this misbehaviour in Chrome while it works in Firefox?
EDIT: More information
I was doing some logging on window.getSelection() and noticed that it contained different information when comparing Chrome and Firefox. Which made the script behave just as it should, but with an argument that's wrong.
The really wierd thing is that when I log window.getSelection() as the very first action of the keyhandler script it behaves like this:
Without any modification I get the "wrong" object.
With a breakpoint set right under the console.log() directive, the script pauses and the log shows a "correct" object, just the way I want it.
The logger:
console.log(window.getSelection());
// Do keyhandling stuff...
Try to make a copy of the original range and add it to the selection rather than the original range at the end of the function, since the range can have dynamic boundaries, and you will end up setting back a "wrong" one.

Categories