Setting selection to edge of node makes WebKit report inconsistent range - javascript

Assume the following DOM tree:
<div id="edit" contenteditable="true">
this content <a id="link" href="http://www.google.com/">contains</a> a link
</div>
Then create a range right after the anchor:
var r = document.createRange();
var link = document.getElementById('link');
r.setStartAfter(link);
r.setEndAfter(link);
As expected, its commonAncestorContainer is the element with id edit:
console.log(r.commonAncestorContainer); /* => <div id="edit" contenteditable="true">…</div> */
Set the selection to this range:
var s = window.getSelection();
s.removeAllRanges();
s.addRange(r);
Now query the window for the current selection range and check its commonAncestorContainer:
var r2 = s.getRangeAt(0);
console.log(r2.commonAncestorContainer);
You will find that in Firefox the result is as expected; the same element with id edit.
In WebKit browsers though, the selection range ancestor container suddenly is the text node inside the anchor; "contains", yet when you start typing you will find that you really are not inside the anchor. WTF!?
Click here for a live demo.
Is there any potential rationale behind this behavior? Any reason to assume that it is not a WebKit bug??
Thanks for your $.02.

WebKit only allows certain positions within the DOM to be used as selection boundaries or caret positions. It therefore modifies a range that is selected using the selection's addRange() method to conform to this. See also https://stackoverflow.com/a/14104166/96100.
There is another issue at play, which is that WebKit has a special case for a caret position at the end of a link which places text typed at that position after the link rather than inside it. This is undeniably a bit of a nasty hack, given that the browser reports the selection as being inside the link. However, this does not happen for other inline elements, as you can see this in this modified version of your demo:
http://jsbin.com/EYoWuWe/7/edit

Related

Set the caret at the end of the content in Froala 2

I'm using Froala 2 and the documentation doesn't seem to have anything that implies a simple way to set the location of the caret, let alone at the beginning or end. I'm trying to seed the editor instance with a little content in certain cases and when I do using html.set, the caret just stays where it is at the beginning and I want to move it to the end. The internet doesn't seem to have anything helpful around this for v2.
Froala support provided an answer for me that works:
var editor = $('#edit').data('froala.editor');
editor.selection.setAtEnd(editor.$el.get(0));
editor.selection.restore();
As far as I know, Froala 2 doesn't provide any API to do this, but you can use native JavaScript Selection API.
This code should do the job:
// Selects the contenteditable element. You may have to change the selector.
var element = document.querySelector("#froala-editor .fr-element");
// Selects the last and the deepest child of the element.
while (element.lastChild) {
element = element.lastChild;
}
// Gets length of the element's content.
var textLength = element.textContent.length;
var range = document.createRange();
var selection = window.getSelection();
// Sets selection position to the end of the element.
range.setStart(element, textLength);
range.setEnd(element, textLength);
// Removes other selection ranges.
selection.removeAllRanges();
// Adds the range to the selection.
selection.addRange(range);
See also:
How to set caret(cursor) position in contenteditable element (div)?
Set caret position at a specific position in contenteditable div

How can I prevent Range.selectNode() selecting too much of the DOM when attempting to select a node injected with appendChild()?

I'm facing an issue with the combination of using appendChild() and Range.selectNode() in JavaScript.
When attempting to use a range to select the newly-appended <textarea> node, it selects too much of the DOM. Copying and pasting the selection seems to just contain a space.
However, if I put the <textarea> node into the DOM from the start (i.e. don't add it with appendChild()) then it works perfectly well and I can copy and paste the selected text as expected.
Note that the CSS isn't really necessary here, but it highlights the fact that the selection contains more than just the <textarea> (or at least it does in Chrome).
HTML:
<div>
<a class="hoverTrigger">Click to trigger textarea element with selected text</a>
</div>
CSS:
.floating {
position: absolute;
}
JavaScript/jQuery (run on DOM ready):
$(".hoverTrigger").click(createAndSelectStuff);
function createAndSelectStuff() {
var textArea = document.createElement("textarea");
textArea.className = "floating";
textArea.value = "Some dynamic text to select";
this.parentNode.appendChild(textArea);
selectObjectText(textArea);
return false;
}
function selectObjectText(container) {
var range = document.createRange();
range.selectNode(container);
window.getSelection().removeAllRanges();
window.getSelection().addRange(range);
}
Here's a jsFiddle.
This is what the resulting selection looks like in Chrome:
How can I stop this happening, and just select the desired text?
Replace your call to selectObjectText with:
container.setSelectionRange(0, container.value.length);
The problem with textarea elements is that they do not hold their contents in DOM nodes. The text value is a property of the element. When you call range.selectNode, what happens is that the range is set so as to encompass the node you pass to the function and the children node of this node, but since a textarea does not store its text in children nodes, then you select only the textarea.
setSelectionRange works with the value of an input element so it does not suffer from this problem. You might want to check the compatibility matrix here to check which browsers support it.

Obtaining a DOM Range by clicking anywhere within an Element

Given the following HTML...
<p>Today is <span data-token="DateTime.DayOfWeek">$$DayOfWeek$$</span>,
</p>
<p>Tomorrow is the next day, etc, etc....</p>
Clicking on $$DayOfWeek$$ returns a DOM Range object (via a component, which is a WYSIWIG editor bundled with KendoUI).
I can then access the entire Element like so...
var element = range.startContainer.parentElement;
console.log(element);
which outputs...
<span data-token="DateTime.DayOfWeek">$$DayOfWeek$$</span>
What i am trying to figure out is how to construct a Range object that consists of the entire Element, as a Range.
The desired 'high level' behaviour is to single click a piece of text, and have the browser select all the text within that element, returning a Range object.
Happy to accept a jQuery solution.
HTML
<p>Today is <span data-token="DateTime.DayOfWeek">$$DayOfWeek$$</span>,</p>
<p>Tomorrow is the next day, etc, etc....</p>
JS
var span = document.querySelector('[data-token]');
span.addEventListener('click', function() {
var sel = window.getSelection();
var range = document.createRange();
sel.removeAllRanges();
range.setStart(span.childNodes[0], 0);
range.setEnd(span.childNodes[0], span.innerText.length);
sel.addRange(range);
});
Here's a fiddle for you:
http://jsfiddle.net/V66zH/2/
It' might not be super cross browser, but works in chrome. See JavaScript Set Window selection for some additional optimizations elsewhere.
Also assumes only one childNode as in your example html
Some additional reference for Ranges (https://developer.mozilla.org/en-US/docs/Web/API/range) and Selections (https://developer.mozilla.org/en-US/docs/Web/API/Selection)
here is a way i came up with that seems to work if i understand you correctly, that you want the element surrounding a click to produce a range containing everything in that element.
without the onclick code, which i assume you can handle, here is the DOM range code you describe:
var sel=document.getSelection(); //find the node that was clicked
var rng=sel.getRangeAt(); //get a range on that node
//now, extend the start and end range to the whole element:
rng.setStart(rng.startContainer.parentNode.firstChild);
rng.setEndAfter(rng.endContainer.parentNode.lastChild);
//DEMO: verify the correct range using a temp div/alert:
var t=document.createElement("div");
t.appendChild(rng.cloneContents());
alert(t.innerHTML);

Javascript get range compared to a parent element

I have a function that return an array (won't work in IE) with two elements
the html code of what the user select inside a div (id=text)
the range of the selection
In case the user select a simple string inside the text div the range return the correct values but when the user select a string inside an element child of div (div#text->p for example) range's values are related to the child element but i want them to be related to the parent (div#text)
Here there's a JsFiddle http://jsfiddle.net/paglia_s/XKjr5/: if you select a string of normal text or normal text + bolded text in the teatarea you'll get the right selection while if you select the bolded word ("am") you'll get the wrong one because the range is related to the child element.
There's a way to do so that the range is always related to div#text?
You could use my Rangy library and its new TextRange module, which provides methods of Range and selection to convert to and from character offsets within the visible text of a container element. For example:
var container = document.getElementById("text");
var sel = rangy.getSelection();
if (sel.rangeCount > 0) {
var range = sel.getRangeAt(0);
var rangeOffsets = range.toCharacterRange(container);
}
rangeOffsets has properties start and end relative to the visible text inside container. The visible text isn't necessarily the same as what jQuery's text() method returns, so you'll need to use Rangy's innerText() implementation. Example:
http://jsfiddle.net/timdown/KGMnq/5/
Alternatively, if you don't want to use Rangy, you could adapt functions I've posted on Stack Overflow before. However, these rely on DOM Range and Selection APIs so won't work on IE < 9.
If you don't want to use a library here is a way which worked for me.
The function returns the cursor offset relative to the textContent of the given node (not in relation to the sub nodes).
Note: The current cursor position must lie in the given node or in any of its sub-nodes.
It's not cross-browser compatible (specially not for IE), but I think it's not much work to fix that as well:
function getCursorPositionInTextOf(element) {
var range = document.createRange(),
curRange = window.getSelection().getRangeAt(0);
range.setStart(element, 0);
range.setEnd(curRange.startContainer, curRange.startOffset);
//Measure the length of the text from the start of the given element to the start of the current range (position of the cursor)
return document.createElement("div").appendChild(range.cloneContents()).textContent.length;
}

how to get selected text, but can I get surrounding context in javascript?

I am able to grab the text that a user has selected on a web page,
using this code:
function getSelected() {
var userSelection;
if (window.getSelection) {
selection = window.getSelection();
} else if (document.selection) {
selection = document.selection.createRange();
}
}
is it posible for me to get the words around the
selected word.
Take these sentences for example: "If you need to
framglubble the zartbox, then you should buy the red widget.
Otherwise you can buy the blue widget and save some money."
My code will tell me if the person has selected the word "widget".
But I'd like to know if the selection is after "red" or "blue". Is
this possible? I've been scouring the Internet for some advice, and
I'm having trouble finding an answer.
thank you for your help
I have written quick script that can identify the part before selection and after selection inside the same DIV element.
However if the same DIV contains the same word more than one time and you select only that word, the current code I wrote can't identify if it's the first or second selected word so bottom line it will not answer your needs.
Anyway, you can see/copy/test the code here: http://jsfiddle.net/kvHxJ/ just select something and see the alert that appears.
If it's enough for your needs after all then great, accept this answer and move on... otherwise I need to know: can we assume the user will select whole words only, one word only? If the answer is yes I do have idea how to go around this.
The way to do this in non-IE browsers is to obtain a Range object from the selection. The range has a start and end boundary, and each boundary of the range is expressed as an offset within a node; if the boundary is within a text node, this offset will be a character offset.
For example, if the following was a text node and the selection is delimited by pipes:
"red |widget| blue widget"
... then the range you'd get from the selection would have a start offset of 4 within the text node.
The following will get you a Range representing the selection and alert the start boundary:
var sel = window.getSelection();
var selectedRange = sel.rangeCount ? sel.getRangeAt(0) : null;
if (range) {
alert("Offset " + selectedRange.startOffset
+ " in node " + selectedRange.startContainer.nodeName);
}
Ranges may be compared to other Ranges, so if you wanted to know, for example, if the current selection came after the word "blue" in the above text node, you could create a Range encompassing the word "blue" and compare it with the selected Range:
// Assume the text node is stored in a variable called textNode
var blueRange = document.createRange();
blueRange.setStart(textNode, 11);
blueRange.setEnd(textNode, 15);
var selectionIsAfterBlue =
(selectedRange.compareBoundaryPoints(Range.END_TO_START, blueRange) == 1);
In IE, none of this works and everything is done differently, generally with much more difficulty. To normalize this to single consistent interface, you could use my Rangy library.
IE has the move set of methods, which reduces this problem to just a couple of lines to expand the selection forward or backward any number of words (see http://www.webreference.com/js/column12/trmethods.html). From there, it's just a matter of comparing text against any arbitrary list of values. Other browsers don't have this feature AFAIK. Fate of the browser wars: one develops an awesome feature ignored or barred by patent from any other, so the feature is forever lost and avoided as burden of cross-browser support for all these innovations inevitably falls squarely on the website designers.
So, below is a generalized function to only get the ID of the parent element of the selected text. And, to work with this cross-browser solution, you have to wrap each word in it's own element complete with unique ID or other attribute. With this setup, it should then be a relatively painless jump to looking ahead and back at sibling or sequentially ID'd/named elements.
The catch here is that the client has to click/drag from the start of the word or phrase to the end, and absolutely no bordering spaces. Even double-clicking on a word will cause it to reference the next element (or in the case of IE, the parent DIV). Additionally, you should add code to restrict the selection boundary to a single parent DIV, as the below code may also expand the selection to surrounding elements. But hopefully you can take fixing that up from here. Otherwise, it's up to using vectors to pinpoint the coordinates of a text compared to all surrounding text.
<script type="text/javascript">
function get_selected_element_id() {
if (window.getSelection) {
// FF
var range = window.getSelection();
}
else if (document.selection) {
// IE
var range = document.selection.createRange();
}
if (range.focusNode) {
// FF
var test_value = range.focusNode.parentNode.id;
}
else {
// IE
var test_value = range.parentElement().id;
}
return test_value;
}
</script>
</head>
<body>
<div id="test_div">
<span id="test1">test</span> <span id="test2">asdf</span> <span id="test3">test2</span> <span id="test4">bla</span>
</div>
<button onclick="alert(get_selected_element_id());">go</button>

Categories