Obtaining a DOM Range by clicking anywhere within an Element - javascript

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);

Related

Retrieve html code of selected text

In mozilla, I can select a text and print the selected text using contentWindow.getSelection(). But I am trying to get the underlying html code block for this selected text. Is there any way I can retrieve it?
I need to extract urls and other informations like src, etc. underneath any clickable text that a user selects. I need the code block of its parent node.
Thanks.
Retrieving the HTML should be relatively easy, but it depends on what you are wanting. window.getSelection() returns a selection object. You can use:
window.getSelection().anchorNode to obtain the Node in which the selection begins and
window.getSelection().focusNode to get the Node in which the selection ends.
For instance:
let selection = contentWindow.getSelection();
let firstElement = selection.anchorNode;
let lastElement = selection.focusNode;
What you do once you have the nodes/elements will depend on what it is that you are actually wanting to find. You have not specified that, so manipulating it past finding those nodes would just be a guess as to what you are wanting. For instance, you just might want to find the parent of the anchorNode, verify that it contains the focusNode (firstElement.parentNode.contains(lastElement)) (if not then continue finding the next parent until it does) and use the parent's innerHTML. Alternately, maybe you want to find the first parent element of the anchorNode which contains the focusNode and then use a TreeWalker to walk the DOM tree until you find the anchorNode and start accumulating the HTML until you encounter the focusNode.
Do you have a mouse event listener or something before you do contentWindow.getSelection?
If you do you can get the selected node by doing:
function onMouseUp(event) {
var aWindow = event.target.ownerDocument.defaultView;
// should test if aWindow is chrome area or actually content area
var contentWindow = aWindow.document instanceof Ci.nsIHTMLDocument ? aWindow : null; // i guessed here but testing if its content window is done in some similar way
if (!contentWindow) { return }
// do contentWindow.getSelection im not familiar with the code, if selection exists // check if more then one range selected then get node for each, however im going to assume only one range is selected
var nodeOfFirstRange = event.explicitOriginalTarget
var elementOfNode = nodeOfFirstRange.parentNode;
var htmlOfElement = elementOfNode.innerHTML;
}
Services.wm.getMostRecentWindow('navigator:browser').gBrowser.addEventListener('mouseup');
issue with this code is if user mouses down in content window and then highlights and mouseup while mouse its outside of content window, like on chrome window or even outside the browser (like if the browser window was not in maximum or if user mousedup in taskbar of os etc) so just use this code as a guide

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.

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 the HTML before, inside, and after a selection (not in textarea)?

Here is what I am trying to accomplish: When a user uses a mouse, keyboard, or touch to select text inside "myDiv" I want to acquire three discreet chunks of HTML: the HTML before the selection (to the "left" of it), the HTML inside the selection, and the HTML after the selection (to the "right" of it). The html should be as it would appear with myDiv.innerHTML.
The selection might start or end inside a tag pair (i.e., the isolated selection isn't necessarily valid HTML). I don't need to deal with special scenarios like absolute-positioned elements within the selection; all of the selections I am concerned with will be constrained to one div that will contain basic tags like strong, em, ul, ol, h1, image, and table.
The closest I've come is using rangy to snag the selection and calling selection.getRangeAt(0).cloneContents() to get the selection HTML. This works well enough until I make a selection that is invalid in isolation, and the browser alters the HTML of the document fragment to make it valid markup.
Extra Information: Here's why I need this:
I am creating a document feedback system, so I need to save the selection information to a database for later retrieval and reconstitution. Normally I would save the selection using the DOM path and the selected text, but the text may change between saving and reconstitution. For example, the author might move entire paragraphs around, delete sections, etc. The DOM path becomes pretty useless then.
So my (imperfect) plan is to store the selection as [offset, length, html_snippet]. That's the "position". I'll also store the html snippets that came directly before and after the selected text. This is the "context".
Using a combination of these data I should be able to relocate the originally selected text most of the time, even if it has moved or partially changed. When that fails, the UI will have a way to address it, but I'd like that to occur as infrequently as possible.
Superthanks!
I have several questions:
1.- When you say 'the html after the selection' - how would that html be any different than the html previous to the selection or viceversa? Is the 'selection' process itself tampering with the html because of your 'script' or whatever?
2.- You said the text selections are not taking place in textareas...what elements are you working with then? paragraphs? divs...? Narrowing it down would help.
3.- Have you thought about using jquery?
http://api.jquery.com/select/
Doing something like
$('#element_with_text_goes_here').select(function() {
//apply grabbing functions here, for example
//copy html 'before' selection:
$pre_html = $('html').clone();
// copy selection...see below:
// copy html 'after' selection'...same as before
});
Copy selection:
As noted here:
Selecting text in an element (akin to highlighting with your mouse)
Jason wrote the following function:
function selectText(element) {
var doc = document;
var text = doc.getElementById(element);
if (doc.body.createTextRange) { // ms
var range = doc.body.createTextRange();
range.moveToElementText(text);
range.select();
} else if (window.getSelection) { // moz, opera, webkit
var selection = window.getSelection();
var range = doc.createRange();
range.selectNodeContents(text);
selection.removeAllRanges();
selection.addRange(range);
}
}
With a live working demo that can be found here:
http://jsfiddle.net/edelman/KcX6A/339/
And a jquery plugin version here:
http://jsfiddle.net/edelman/KcX6A/340/
Which you can use for the obtention of the selected text. You'll just have to tweak it accordingly since he was approaching it from a reversed angle.
The more details you can give us...the better we can help.
Hope this helps
G
This code gets html/text from user's selection, but it works in IE only. The code works with cross-tag selection too. (Globals used to keep the code short.)
<script>
function selected(){
thediv=document.getElementById('div');
res=document.getElementById('htm');
userSelection=document.selection;
userRange=userSelection.createRange();
/* For wider scale of elements */
// rangeParent=userRange.parentElement();
// if(rangeParent!=thediv) userRange.moveToElementText(rangeParent);
rangeText=userRange.htmlText; // OR: rangeText=userRange.text;
res.innerText=rangeText;
return;
}
</script>
</head>
<body onload="document.onselectionchange=selected;">
<div id="div">
<h1>The great testpage</h1>
<p>A paragraph with some text</p>
<p>This paragraph <b>contains</b> a child element.</p>
<p>And this is the last paragraph.</p>
<table>
<tr><td>Cell1-1</td><td>cell1-2</td></tr>
<tr><td>Cell2-1</td><td>cell2-2</td></tr>
</table>
<ol>
<li>item1</li>
<li>item2</li>
<li>item3</li>
</ol>
</div>
<br>
<span id="htm"></span>
</body>
Content before&after selection in the thediv you'll get like this: prepost=thediv.innerHTML/innerText.split(rangeText);
If the page contains any other elements but thediv, they have to be made unselectable.

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